<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[10 PRINT "I AM COOL"]]></title><description><![CDATA[I've been coding since 1982, when my dad bought a 1K ZX81. I've been paid to code since 1991, journeying from mainframe development, through PC development, and]]></description><link>https://www.10printiamcool.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1630085815277/mI80u9XVN.jpeg</url><title>10 PRINT &quot;I AM COOL&quot;</title><link>https://www.10printiamcool.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 17 May 2026 21:29:57 GMT</lastBuildDate><atom:link href="https://www.10printiamcool.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Reacting to Blob Storage events in Azure Functions]]></title><description><![CDATA[In the previous post in this series on creating a serverless webhook proxy, I looked at connecting to Azure Blob Storage without connection strings. Now that we have the webhook payloads validated and stored, it is time to look at processing them. So...]]></description><link>https://www.10printiamcool.com/reacting-to-blob-storage-events-in-azure-functions</link><guid isPermaLink="true">https://www.10printiamcool.com/reacting-to-blob-storage-events-in-azure-functions</guid><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[azure-blobstorage]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 01 Dec 2024 09:17:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2hxEa_at8oI/upload/2691eaf9438ae9cd7f24daf56ab70c0e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous <a target="_blank" href="https://www.10printiamcool.com/connecting-to-cloud-azure-blob-storage">post</a> in this <a target="_blank" href="https://www.10printiamcool.com/series/azure-serverless">series</a> on creating a serverless webhook proxy, I looked at connecting to Azure Blob Storage without connection strings. Now that we have the webhook payloads validated and stored, it is time to look at processing them. So, the first step is reacting to the events raised by Azure Blob Storage.</p>
<h2 id="heading-blob-container-polling-vs-event-grid-events">Blob container polling vs. Event Grid events</h2>
<p>The first choice in our journey is how to react to the payloads being stored. The choice is between a polling-based approach and an event-based approach.</p>
<p>The polling approach is covered in the Microsoft article <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&amp;pivots=programming-language-csharp">Azure Blob storage trigger for Azure Functions</a>.</p>
<p>Below is an abbreviated version of the example given. The code uses the <code>BlobTrigger</code> attribute with the path to match on specified in the constructor:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">BlobFunction</span>
{
    [<span class="hljs-meta">Function(nameof(BlobFunction))</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Run</span>(<span class="hljs-params">
        [BlobTrigger(<span class="hljs-string">"test-samples-trigger/{name}"</span></span>)] <span class="hljs-keyword">string</span> myTriggerItem)</span>
    {
        <span class="hljs-comment">// &lt;snip&gt;</span>
    }
}
</code></pre>
<p>However, I noted the following 'Tip':</p>
<blockquote>
<p>There are several ways to execute your function code based on changes to blobs in a storage container. If you choose to use the Blob storage trigger, note that there are two implementations offered: a polling-based one (referenced in this article) and an event-based one. It is recommended that you use the event-based implementation as it has lower latency than the other.</p>
</blockquote>
<p>This tip also included a link to the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-event-grid-blob-trigger?pivots=programming-language-csharp">Trigger Azure Functions on blob containers using an event subscription</a> Microsoft tutorial.</p>
<p>After reading up on both, I decided to go down the Event Grid route. Using events felt more decoupled than having functions polling the blob container. Plus the lower latency would be an advantage when delivering the webhook payloads.</p>
<h2 id="heading-creating-an-event-grid-function">Creating an Event Grid function</h2>
<p>The next step was to use the Visual Studio wizard to create the Azure function. This function will be triggered on accepted messages and do the following two steps:</p>
<ol>
<li>Deduplicate messages based on the payload</li>
<li>Forward messages to the appropriate endpoint</li>
</ol>
<p>In this post, I won't be covering the first step. We will just look at triggering the function and using the trigger to retrieve the payload and forward it on.</p>
<p>With this functionality in mind, I used the Visual Studio wizard to create a new Azure function called <code>DedupeAndForwardFunction</code> and selected the option for 'Event Grid trigger'. The resulting code is shown below.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DedupeAndForwardFunction</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ILogger&lt;DedupeAndForwardFunction&gt; _logger;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DedupeAndForwardFunction</span>(<span class="hljs-params">ILogger&lt;DedupeAndForwardFunction&gt; logger</span>)</span>
    {
        _logger = logger;
    }

    [<span class="hljs-meta">Function(nameof(DedupeAndForwardFunction))</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Run</span>(<span class="hljs-params">[EventGridTrigger] CloudEvent cloudEvent</span>) <span class="hljs-comment">// &lt;-- CloudEvent?</span></span>
    {
        _logger.LogInformation(
            <span class="hljs-string">"Event type: {type}, Event subject: {subject}"</span>,
            cloudEvent.Type, cloudEvent.Subject);
    }
}
</code></pre>
<p>What caught my eye was the type of event parameter. What is a <code>CloudEvent</code>?</p>
<h2 id="heading-cloud-events-vs-event-grid-events">Cloud Events vs Event Grid Events</h2>
<p>A bit of searching led me to the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/event-grid/cloud-event-schema">CloudEvents v1.0 schema with Azure Event Grid</a> Microsoft article that states:</p>
<blockquote>
<p>Azure Event Grid natively supports events in the JSON implementation of CloudEvents v1.0 and HTTP protocol binding. CloudEvents is an open specification for describing event data. CloudEvents simplifies interoperability by providing a common event schema for publishing, and consuming cloud based events. This schema allows for uniform tooling, standard ways of routing &amp; handling events, and universal ways of deserializing the outer event schema. With a common schema, you can more easily integrate work across platforms.</p>
</blockquote>
<p>Looking a bit deeper, the key differences appear to be:</p>
<ol>
<li>Schema Structure:<ul>
<li>Event Grid: Uses Azure-specific field names (<code>topic</code>, <code>eventType</code>)</li>
<li>Cloud Events: Uses standardised field names (<code>source</code>, <code>type</code>)</li>
</ul>
</li>
<li>Version Handling:<ul>
<li>Event Grid: Has separate <code>dataVersion</code> and <code>metadataVersion</code> fields</li>
<li>Cloud Events: Uses single <code>specversion</code> field</li>
</ul>
</li>
</ol>
<p>Although Visual Studio pushed me down the <code>CloudEvent</code> route, I decided to revert to using <code>EventGridEvent</code>. This was because I would be working within the Azure ecosystem and the examples and documentation also favour the use of <code>EventGridEvent</code>.</p>
<p>So, the function code became the following, with an extra statement added to output the event as JSON.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">Function(nameof(DedupeAndForwardFunction))</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Run</span>(<span class="hljs-params">[EventGridTrigger] EventGridEvent eventGridEvent</span>)</span>
{
    _logger.LogInformation(
        <span class="hljs-string">"Event type: {eventType}, Event subject: {subject}"</span>,
        eventGridEvent.EventType, eventGridEvent.Subject); <span class="hljs-comment">// &lt;-- EventType</span>

    _logger.LogDebug(
        <span class="hljs-string">"EVENT: {event}"</span>,
        JsonSerializer.Serialize(eventGridEvent));
}
</code></pre>
<p>For more information on Cloud Events, see <a target="_blank" href="https://cloudevents.io/">cloudevents.io</a>.</p>
<h2 id="heading-hooking-up-the-function-to-events">Hooking up the function to events</h2>
<p>The next step was to get events from Blob Storage triggering the Azure Function. To do this, I navigated to the storage account in the Azure Portal and clicked the 'Event Subscription' button.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-05-blob-events/adding-a-storage-account-event-subscription-in-the-portal.png?raw=true" alt="Adding a storage account event subscription in the portal" /></p>
<p>I was then prompted to create an event subscription, specifying the name of the subscription and the schema type. It is here that you choose whether the event will be in the Event Grid schema or Cloud Event schema.</p>
<p>I was also prompted for the name to the topic to which the storage account will publish events. It turns out that a storage account can only publish to one topic, so given this I chose a name to reflect the storage account. If you create further event subscriptions, then you are not prompted for the topic name again.</p>
<p>Below the topic name, you can select which events are to be published. In my case, I only wanted 'Blob Created' events. Finally, you select the endpoint for the event, which is the <code>DedupeAndForwardFunction</code> Azure Function I created and published earlier.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-05-blob-events/create-event-subscription-in-the-portal.png?raw=true" alt="Create event subscription in the portal" /></p>
<p>With the event subscription in place, I sent a valid request to the <code>ValidateAndStoreFunction</code> Azure Function. The resulting Blob Storage event trigged the <code>DedupeAndForwardFunction</code> and the event below was logged.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"0d666914-901e-0082-26b5-3ac1de067d1c"</span>,
  <span class="hljs-attr">"topic"</span>: <span class="hljs-string">"&lt;snip&gt;"</span>,
  <span class="hljs-attr">"subject"</span>: <span class="hljs-string">"/blobServices/default/containers/webhook-payloads-accepted/blobs/LovelyLoans/QuickValuationCo/2024-11-19/2024-11-19T18:58:31UTC-be2f0960-ff56-4cab-8465-9ebd0f6b1d5c.json"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"api"</span>: <span class="hljs-string">"PutBlob"</span>,
    <span class="hljs-attr">"clientRequestId"</span>: <span class="hljs-string">"d630505b-bffd-4419-b2a7-f8f109eb275f"</span>,
    <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"0d666914-901e-0082-26b5-3ac1de000000"</span>,
    <span class="hljs-attr">"eTag"</span>: <span class="hljs-string">"0x8DD08CC29D75EE3"</span>,
    <span class="hljs-attr">"contentType"</span>: <span class="hljs-string">"application/octet-stream"</span>,
    <span class="hljs-attr">"contentLength"</span>: <span class="hljs-number">2266</span>,
    <span class="hljs-attr">"blobType"</span>: <span class="hljs-string">"BlockBlob"</span>,
    <span class="hljs-attr">"accessTier"</span>: <span class="hljs-string">"Default"</span>,
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;snip&gt;/webhook-payloads-accepted/LovelyLoans/QuickValuationCo/2024-11-19/2024-11-19T18:58:31UTC-be2f0960-ff56-4cab-8465-9ebd0f6b1d5c.json"</span>,
    <span class="hljs-attr">"sequencer"</span>: <span class="hljs-string">"0000000000000000000000000003C5ED00000000002bc82a"</span>,
    <span class="hljs-attr">"storageDiagnostics"</span>: {
      <span class="hljs-attr">"batchId"</span>: <span class="hljs-string">"a7d80882-4006-005a-00b5-3ae687000000"</span>
    }
  },
  <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"Microsoft.Storage.BlobCreated"</span>,
  <span class="hljs-attr">"eventTime"</span>: <span class="hljs-string">"2024-11-19T18:58:32.4819404Z"</span>,
  <span class="hljs-attr">"dataVersion"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p>This was all good, but I then sent an invalid request. As I expected, a Blob Storage event trigged the <code>DedupeAndForwardFunction</code> Azure Function even though the design requires that only valid requests are forwarded. We can see this by the <code>webhook-payloads-rejected</code> in the <code>subject</code>.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"b9eb1363-a01e-001f-2bb5-3a3364069d50"</span>,
  <span class="hljs-attr">"topic"</span>: <span class="hljs-string">"&lt;snip&gt;"</span>,
  <span class="hljs-attr">"subject"</span>: <span class="hljs-string">"/blobServices/default/containers/webhook-payloads-rejected/blobs/LovelyLoans/QuickValuationCo/2024-11-19/2024-11-19T19:02:58UTC-4ea825da-ac01-4cf4-bcca-86f8d1d36d6d.json"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"api"</span>: <span class="hljs-string">"PutBlob"</span>,
    <span class="hljs-attr">"&lt;snip&gt;"</span>: <span class="hljs-string">"..."</span>
  },
  <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"Microsoft.Storage.BlobCreated"</span>,
  <span class="hljs-attr">"eventTime"</span>: <span class="hljs-string">"2024-11-19T19:02:58.8654233Z"</span>,
  <span class="hljs-attr">"dataVersion"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p>One possible solution would be to amend the <code>DedupeAndForwardFunction</code> Azure Function to introspect the event and ignore anything not from the <code>webhook-payloads-accepted</code> container. However, this would have cost each time it ran. A better way is to add an event filter.</p>
<h2 id="heading-adding-an-event-filter">Adding an event filter</h2>
<p>I went back into the Azure Portal and edited the subscription. Selecting the 'Filters' tab, I enabled subject filtering and added a prefix to match on <code>/blobServices/default/containers/webhook-payloads-accepted/blobs</code>. This is where having an example event from Azure proved very useful.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-05-blob-events/adding-a-subscription-filter-in-the-portal.png?raw=true" alt="Adding a subscription filter in the portal" /></p>
<p>With the subscription updated, I ran my tests. First checking that a valid request triggered the function, then a second test checking that only the first Azure Function was triggered. Sure enough, all I saw in the logs was the following.</p>
<pre><code class="lang-text">2024-11-19T19:12:32Z   [Information]   Executing 'Functions.ValidateAndStoreFunction' (Reason='This function was programmatically called via the host APIs.', Id=ab31e19f-eb17-42f1-891b-ae2bdb80bdab)
2024-11-19T19:12:32Z   [Information]   Executed 'Functions.ValidateAndStoreFunction' (Succeeded, Id=ab31e19f-eb17-42f1-891b-ae2bdb80bdab, Duration=16ms)
</code></pre>
<p>It would still be worth putting a check in the <code>DedupeAndForwardFunction</code> Azure Function. It should protect itself from misconfiguration, but log an error to indicate that there is a misconfiguration.</p>
<p>The Microsoft <a target="_blank" href="https://learn.microsoft.com/en-gb/azure/event-grid/subscription-creation-schema?WT.mc_id=Portal-Microsoft_Azure_EventGrid#filter-object">Event subscription filter object</a> article covers the various options available for filtering. One thing I did note, was that there is no support for wildcards.</p>
<h2 id="heading-debugging-locally">Debugging locally</h2>
<p>Now I had my Azure Function triggering, I could start on the actual functionality. Developing this would be much easier if I could run my code locally. A bit of searching turned up the article <a target="_blank" href="https://harrybellamy.com/posts/debugging-azure-function-event-grid-triggers-locally/">Debugging Azure Function Event Grid Triggers Locally</a>.</p>
<p>This did point me in the right direction, but I could not get it to work. More searching then turned up the following two articles:</p>
<ul>
<li><a target="_blank" href="https://stackoverflow.com/questions/77543838/unable-to-debug-event-grid-trigger-azure-function-locally">Unable to debug Event Grid Trigger Azure function locally</a></li>
<li><a target="_blank" href="https://github.com/Azure/Azure-Functions/issues/2426">Azure Event Grid Trigger function is not working locally</a></li>
</ul>
<p>These both highlighted that this comment, added by Visual Studio to the boilerplate Azure Function code, is misleading.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Default URL for triggering event grid function in the local environment.</span>
<span class="hljs-comment">// http://localhost:7071/runtime/webhooks/EventGrid?functionName={functionname}</span>
</code></pre>
<p>The port number is not always <code>7071</code>. To find out what it is, you need to look in the <code>launchSettings.json</code> file.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-05-blob-events/launch-settings-file-in-visual-studio.png?raw=true" alt="Launch settings file in Visual Studio" /></p>
<p>Here I could see that, in my case, the port was <code>7089</code>.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"profiles"</span>: {
    <span class="hljs-attr">"WebhookFunctionApp"</span>: {
      <span class="hljs-attr">"commandName"</span>: <span class="hljs-string">"Project"</span>,
      <span class="hljs-attr">"commandLineArgs"</span>: <span class="hljs-string">"--port 7089"</span>,
      <span class="hljs-attr">"launchBrowser"</span>: <span class="hljs-literal">false</span>
    }
  }
}
</code></pre>
<p>With this knowledge, I was able to hit <code>F5</code> and trigger my Azure Function locally with the following request. Note that the <code>aeg-event-type: Notification</code> header is required for this to work.</p>
<pre><code class="lang-text">POST http://localhost:7089/runtime/webhooks/EventGrid?functionName=EventGridFunction
content-type: application/json
aeg-event-type: Notification
</code></pre>
<pre><code class="lang-json">{
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"0d666914-901e-0082-26b5-3ac1de067d1c"</span>,
  <span class="hljs-attr">"subject"</span>: <span class="hljs-string">"/blobServices/default/containers/webhook-payloads-accepted/blobs/LovelyLoans/QuickValuationCo/2024-11-19/2024-11-19T18:58:31UTC-be2f0960-ff56-4cab-8465-9ebd0f6b1d5c.json"</span>,
  <span class="hljs-attr">"&lt;snip&gt;"</span>: <span class="hljs-string">"..."</span>,
  <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"Microsoft.Storage.BlobCreated"</span>,
  <span class="hljs-attr">"eventTime"</span>: <span class="hljs-string">"2024-11-19T19:02:58.8654233Z"</span>,
  <span class="hljs-attr">"dataVersion"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p>See also, the Microsoft article <a target="_blank" href="https://learn.microsoft.com/en-us/azure/communication-services/how-tos/event-grid/local-testing-event-grid">Test your Event Grid handler locally</a> for more details local testing.</p>
<h2 id="heading-processing-system-events">Processing system events</h2>
<p>The final step for this blog post was to process the events and forward the request on to a downstream endpoint. Guided by the Microsoft <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/overview/azure/messaging.eventgrid-readme?view=azure-dotnet#deserializing-event-data">Azure Event Grid client library</a> article, I was pointed to the <code>TryGetSystemEventData()</code> method and the following code.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">foreach</span> (EventGridEvent egEvent <span class="hljs-keyword">in</span> egEvents)
{
    <span class="hljs-comment">// If the event is a system event, TryGetSystemEventData will return the deserialized system event</span>
    <span class="hljs-keyword">if</span> (egEvent.TryGetSystemEventData(<span class="hljs-keyword">out</span> <span class="hljs-keyword">object</span> systemEvent))
    {
        <span class="hljs-keyword">switch</span> (systemEvent)
        {
            <span class="hljs-keyword">case</span> SubscriptionValidationEventData subscriptionValidated:
                Console.WriteLine(subscriptionValidated.ValidationCode);
                <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> StorageBlobCreatedEventData blobCreated:
                Console.WriteLine(blobCreated.BlobType);
                <span class="hljs-keyword">break</span>;
            <span class="hljs-comment">// Handle any other system event type</span>
            <span class="hljs-keyword">default</span>:
                Console.WriteLine(egEvent.EventType);
                <span class="hljs-comment">// we can get the raw Json for the event using Data</span>
                Console.WriteLine(egEvent.Data.ToString());
                <span class="hljs-keyword">break</span>;
        }
    }
    <span class="hljs-keyword">else</span>
    {
        <span class="hljs-comment">// &lt;snip&gt;</span>
    }
}
</code></pre>
<p>Note here the handling of <code>SubscriptionValidationEventData</code>. When Azure Event Grid creates a webhook subscription, it sends a subscription validation event to verify the endpoint. The endpoint needs to respond with a specific validation response to confirm the webhook endpoint. However, it turns out you don't need to manually return the validation response for an Azure Function <code>EventGridTrigger</code>, as the Azure Functions runtime handles this automatically. We do still have to handle the event.</p>
<p>Now that I knew how to handle the incoming events, I was able to extract the Blob Storage URL from the event, load the payload from Blob Storage, then use the details to resolve to the downstream endpoint and invoke the endpoint with the body of the original request.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">switch</span> (systemEvent)
{
    <span class="hljs-keyword">case</span> SubscriptionValidationEventData subscriptionValidated:
        _logger.LogInformation(
            <span class="hljs-string">"subscriptionValidated.ValidationCode: {code}"</span>,
            subscriptionValidated.ValidationCode);
        <span class="hljs-keyword">break</span>;

    <span class="hljs-keyword">case</span> StorageBlobCreatedEventData blobCreated:
        _logger.LogDebug(
            <span class="hljs-string">"blobCreated.Url: {url}"</span>,
            blobCreated.Url);

        <span class="hljs-keyword">var</span> acceptedPayload =
            <span class="hljs-keyword">await</span> _payloadStore.GetAcceptedPayloadAsync(blobCreated.Url);

        <span class="hljs-keyword">var</span> endpointProxy =
            _endpointProxyFactory.GetEndpointProxy(
                acceptedPayload.TenantId, acceptedPayload.ContractId);

        <span class="hljs-keyword">await</span> endpointProxy.InvokeAsync(acceptedPayload.Body);

        <span class="hljs-keyword">break</span>;

    <span class="hljs-keyword">default</span>:
        _logger.LogError(
            <span class="hljs-string">"Unhandled event type: {eventType}"</span>,
            eventGridEvent.EventType);
        <span class="hljs-keyword">break</span>;
}
</code></pre>
<p>I was able to take advantage of the work I did in my <a target="_blank" href="https://www.10printiamcool.com/connecting-to-cloud-azure-blob-storage">Connecting to cloud Azure Blob Storage</a> post, which introduced an abstraction over Blob Storage that decouples the code from whether using local or cloud storage and avoids any connection strings.</p>
<p>This allowed me to test my function locally, by amending the <code>url</code> in the event to point to a local storage URL.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"&lt;snip&gt;"</span>: <span class="hljs-string">"..."</span>,
  <span class="hljs-attr">"subject"</span>: <span class="hljs-string">"/devstoreaccount1/webhook-payloads-accepted/LovelyLoans/QuickValuationCo/2024-04-30/2024-04-30T18:13:03UTC-e2a8e023-079c-4ce5-ac0f-2021264f92fe.json"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"&lt;snip&gt;"</span>: <span class="hljs-string">"..."</span>,
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"http://127.0.0.1:10000/devstoreaccount1/webhook-payloads-accepted/LovelyLoans/QuickValuationCo/2024-04-30/2024-04-30T18:13:03UTC-e2a8e023-079c-4ce5-ac0f-2021264f92fe.json"</span>
  }
}
</code></pre>
<h2 id="heading-summary-and-next-steps">Summary and next steps</h2>
<p>In this post, I looked at the options available for Azure Functions to react to Blob Storage events. I chose the event-driven option over polling. I also had to choose between Event Grid events and Cloud Events. Both of these choices highlights how often in the evolving cloud world, the software architect is faced with such dilemmas. What was considered best practice one day, may well not be the next.</p>
<p>I was able to use to the Azure Portal to set up a subscription with a filter, have my Azure Function handling the events, and then forwarding on the original webhook calls. I was also able to test my function locally, after working out that the Visual Studio boilerplate code had mislead me.</p>
<p>To simplify the post, I did skip over was error handling and the deduplication of resent events. Both of these are critical to any code approaching production-level quality. I intend to return to these aspects in later posts.</p>
]]></content:encoded></item><item><title><![CDATA[Using composition over inheritance to create an OpenAPI client]]></title><description><![CDATA[In the previous parts in this series, I discovered the OpenAPI package, used it to verify REST API payloads, and then used it to create a class that can use an OpenAPI specification to become a dynamic API client. At the end of the last part, I compa...]]></description><link>https://www.10printiamcool.com/using-composition-over-inheritance-to-create-an-openapi-client</link><guid isPermaLink="true">https://www.10printiamcool.com/using-composition-over-inheritance-to-create-an-openapi-client</guid><category><![CDATA[C#]]></category><category><![CDATA[.NET]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[swagger]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 27 Oct 2024 09:33:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/5kAEhOx90DA/upload/483fd20129964374af625a74c653c4c7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous parts in this <a target="_blank" href="https://www.10printiamcool.com/series/adventures-with-openapi">series</a>, I discovered the <a target="_blank" href="https://www.nuget.org/packages/Microsoft.OpenApi">OpenAPI package</a>, used it to verify <a target="_blank" href="https://restfulapi.net/">REST API</a> payloads, and then used it to create a class that can use an <a target="_blank" href="https://swagger.io/specification/">OpenAPI specification</a> to become a dynamic API client. At the end of the <a target="_blank" href="https://www.10printiamcool.com/using-microsoftopenapireader-to-create-a-dynamic-rest-api-client">last part</a>, I compared the resulting client with a static client generated by <a target="_blank" href="https://visualstudio.microsoft.com/">Visual Studio</a>. I liked the strong-typing of the static approach, but didn't like the quantity of generated code I would have to own.</p>
<p>With this in mind, I wondered if I could create a client class that is a hybrid of the two. An example usage is shown below.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> client =
    <span class="hljs-keyword">await</span> PetstoreHybridOpenApiClient.CreateAsync(
        <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">"http://petstore.swagger.io"</span>));

<span class="hljs-keyword">await</span> client.AddPetAsync(<span class="hljs-keyword">new</span> Pet { Name = <span class="hljs-string">"Luna"</span> });

Pet pet = <span class="hljs-keyword">await</span> client.GetPetByIdAsync(<span class="hljs-number">1</span>);

ICollection&lt;Pet&gt; pets = <span class="hljs-keyword">await</span> client.FindPetsByStatusAsync([Anonymous.Sold]);
</code></pre>
<p>As an additional challenge, I decided to try implementing it all without resorting to inheritance. As someone who has used object orientation for a long time, my first thought is often to start creating subclasses. However, I am aware of the option to use <a target="_blank" href="https://en.wikipedia.org/wiki/Composition_over_inheritance">composition over inheritance</a>. So I thought it would be interesting to try that and see how it felt.</p>
<h2 id="heading-creating-the-constructors-and-factory-methods">Creating the constructors and factory methods</h2>
<p>The dynamic client class that I developed has a factory method that is used as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> client = <span class="hljs-keyword">await</span> OpenApiClientV2.CreateAsync(openApiJson, domainUri);
</code></pre>
<p>This relies on the calling code reading the OpenAPI document into a JSON string and passing it to the method. For the hybrid client, I decided that I would use a convention instead. The OpenAPI document would be an embedded resource, with the same name as the client class, but with a <code>.OpenAPI.json</code> extension.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/openapi-document-file-properties.png?raw=true" alt="Properties of the OpenAPI document file" /></p>
<p>This meant that the factory method calling code would be simplified, with just the domain URI being required.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> client =
    <span class="hljs-keyword">await</span> PetstoreHybridOpenApiClient.CreateAsync(
        <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">"http://petstore.swagger.io"</span>));
</code></pre>
<p>With this usage in mind, I created the skeleton for the <code>PetstoreHybridClient</code> class, wrapping an <code>OpenApiClientV2</code> instance in a 'has-a' relationship.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PetstoreHybridClient</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> OpenApiClientV2 _client;

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-title">PetstoreHybridClient</span>(<span class="hljs-params">OpenApiClientV2 client</span>)</span>
    {
        _client = client;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;PetstoreHybridClient&gt; <span class="hljs-title">CreateAsync</span>(<span class="hljs-params">Uri domainUri</span>)</span>
    {
        <span class="hljs-comment">// Instantiate an instance of PetstoreHybridClient...</span>
    }
}
</code></pre>
<p>The next task was for the factory method to create and return an instance. To do this, it delegates to a method on a new static class <code>HybridOpenApiClient</code>. The method takes the domain and a function that receives an <code>OpenApiClientV2</code> instance and instantiates a new class. In this case, the <code>PetstoreHybridClient</code> class.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;PetstoreHybridClient&gt; <span class="hljs-title">CreateAsync</span>(<span class="hljs-params">Uri domainUri</span>)</span>
{
    <span class="hljs-keyword">return</span>
        <span class="hljs-keyword">await</span> HybridOpenApiClient.CreateAsync(
            domainUri, (client) =&gt; <span class="hljs-keyword">new</span> PetstoreHybridClient(client));
}
</code></pre>
<p>The <code>HybridOpenApiClient</code> class was then implemented as follows. Note that, the <code>createHybridClient</code> function would not be necessary if C# <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters">type constraints</a> could specify anything other that a default constructor.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HybridOpenApiClient</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> <span class="hljs-title">Task</span>&lt;<span class="hljs-title">T</span>&gt; <span class="hljs-title">CreateAsync</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">
        Uri domainUri, Func&lt;OpenApiClientV2, T&gt; createHybridClient</span>) <span class="hljs-keyword">where</span> T : class</span>
    {
        <span class="hljs-keyword">var</span> openApiJson = LoadOpenApiJsonForType&lt;T&gt;();

        <span class="hljs-keyword">var</span> client = <span class="hljs-keyword">await</span> OpenApiClientV2.CreateAsync(openApiJson, domainUri);

        <span class="hljs-keyword">return</span> createHybridClient(client);
    }
}
</code></pre>
<p>I have omitted the details for the <code>LoadOpenApiJsonForType</code>, but the full code can be found on the accompanying <a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/OpenApiDynamicClient/OpenApiDynamicClient/HybridOpenApiClient.cs">GitHub repo</a>.</p>
<h2 id="heading-adding-the-api-methods">Adding the API methods</h2>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">AddPetAsync</span>(<span class="hljs-params">
    Pet body</span>)</span>
{
    <span class="hljs-keyword">await</span> _client.PerformAsync(
        <span class="hljs-string">"addPet"</span>,
        [<span class="hljs-meta">
            (<span class="hljs-meta-string">"body"</span>, JsonConvert.Serialize(body)),
        </span>]);
}
</code></pre>
<p>In the <a target="_blank" href="https://www.10printiamcool.com/using-microsoftopenapireader-to-create-a-dynamic-rest-api-client">previous post in the series</a>, I used Visual Studio to generate a client and a set of model classes. These model classes are annotated with attributes that implement the validation in the OpenAPI document. For example, the <code>Name</code> property on the <code>Pet</code> class is annotated as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Pet</span>
{
    <span class="hljs-comment">// &lt;snip&gt;</span>

    [<span class="hljs-meta">Newtonsoft.Json.JsonProperty(<span class="hljs-meta-string">"name"</span>, Required = Newtonsoft.Json.Required.Always)</span>]
    [<span class="hljs-meta">System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Name { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-comment">// &lt;snip&gt;</span>
}
</code></pre>
<p>The problem with this was that the <code>JsonConvert.Serialize(body)</code> results in a <code>JsonSerializationException</code>, which I didn't want. I wanted all failures to be handled the same. However, on the other hand, I wanted to use the generated classes.</p>
<p>The solution was to create a pair of serialization helpers as follows:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> JsonSerializerSettings _serializerSettings =
    <span class="hljs-keyword">new</span>()
    {
        MissingMemberHandling = MissingMemberHandling.Ignore,
        NullValueHandling = NullValueHandling.Ignore,
        Converters = [<span class="hljs-keyword">new</span> StringEnumConverter()],
    };

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span> <span class="hljs-title">Serialize</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> <span class="hljs-keyword">value</span></span>)</span>
{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">value</span> == <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">var</span> valueJson = JsonConvert.SerializeObject(<span class="hljs-keyword">value</span>, _serializerSettings);

    <span class="hljs-keyword">var</span> isJsonString = valueJson.StartsWith(<span class="hljs-string">"\""</span>);
    <span class="hljs-keyword">if</span> (isJsonString) <span class="hljs-keyword">return</span> valueJson.Trim(<span class="hljs-string">'"'</span>);

    <span class="hljs-keyword">return</span> valueJson;
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> T <span class="hljs-title">Deserialize</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">JsonResponse response</span>)</span>
{
    <span class="hljs-keyword">return</span> JsonConvert.DeserializeObject&lt;T&gt;(response.Payload, _serializerSettings);
}
</code></pre>
<p>These could then be used as follows in the hybrid client.</p>
<pre><code class="lang-csharp">  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;Pet&gt; <span class="hljs-title">GetPetByIdAsync</span>(<span class="hljs-params">
      <span class="hljs-keyword">long</span> petId</span>)</span>
  {
      <span class="hljs-keyword">var</span> response =
          <span class="hljs-keyword">await</span> _client.PerformAsync(
              <span class="hljs-string">"getPetById"</span>,
              [<span class="hljs-meta">
                  (<span class="hljs-meta-string">"petId"</span>, HybridOpenApiClient.Serialize(petId)),
              </span>]);

      <span class="hljs-keyword">return</span> HybridOpenApiClient.Deserialize&lt;Pet&gt;(response);
  }
</code></pre>
<h2 id="heading-adding-and-overriding-default-behaviour">Adding and overriding default behaviour</h2>
<p>The <code>OpenApiClientV2</code> class allows custom error handling. This is done by supplying an <code>OnFailure</code> function. For the hybrid client, I wanted the base implementation to throw an exception for any failure. To do this, I extended <code>HybridOpenApiClient</code> with the following code.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HybridOpenApiClient</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> <span class="hljs-title">Task</span>&lt;<span class="hljs-title">T</span>&gt; <span class="hljs-title">CreateAsync</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">
        Uri domainUri, Func&lt;OpenApiClientV2, T&gt; createHybridClient</span>) <span class="hljs-keyword">where</span> T : class</span>
    {
        <span class="hljs-comment">// &lt;snip&gt;</span>

        client.OnFailure = OnFailure;

        <span class="hljs-keyword">return</span> createHybridClient(client);
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnFailure</span>(<span class="hljs-params">
        <span class="hljs-keyword">string</span> operationId,
        IEnumerable&lt;(<span class="hljs-keyword">string</span>, <span class="hljs-keyword">string</span></span>)&gt; parameters,
        JsonResponse response)</span>
    {
        <span class="hljs-keyword">if</span> (response.HttpStatusCode.HasValue)
        {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> OpenApiException(
                <span class="hljs-string">$"<span class="hljs-subst">{operationId}</span> received <span class="hljs-subst">{(<span class="hljs-keyword">int</span>)response.HttpStatusCode}</span>: "</span> +
                    <span class="hljs-string">$"<span class="hljs-subst">{<span class="hljs-keyword">string</span>.Join(<span class="hljs-string">", "</span>, response.FailureReasons)}</span>"</span>,
                response.HttpStatusCode.Value);
        }

        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> OpenApiException(
            <span class="hljs-string">$"<span class="hljs-subst">{operationId}</span> failed: "</span> +
            <span class="hljs-string">$"<span class="hljs-subst">{<span class="hljs-keyword">string</span>.Join(<span class="hljs-string">", "</span>, response.FailureReasons)}</span>"</span>,
            response.Exception);
    }
}
</code></pre>
<p>So now, any failure results in a <code>OpenApiException</code> being thrown with a description of the failure reason. As an exercise, I wanted the specific implementation, <code>PetstoreHybridClient</code>, to also log the failure details.</p>
<p>This was possible by supplying a custom <code>OnFailure</code> implementation as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-title">PetstoreHybridClient</span>(<span class="hljs-params">OpenApiClientV2 client</span>)</span>
{
    _client = client;

    _client.OnFailure =
        (o, p, r) =&gt;
            {
                LogFailure(o, r);
                HybridOpenApiClient.OnFailure(o, p, r);
            };
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">LogFailure</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> operationId, JsonResponse response</span>)</span>
{
    Console.WriteLine(
        <span class="hljs-string">$"<span class="hljs-subst">{operationId}</span> failed in <span class="hljs-subst">{response.ElapsedMilliseconds}</span>ms"</span>);
}
</code></pre>
<p>With this, the <code>PetstoreHybridClient</code> logs the failure, but retains the behaviour of throwing exceptions.</p>
<h2 id="heading-comparison-with-a-base-client">Comparison with a base client</h2>
<p>For comparison, I also created an abstract class <code>HybridOpenApiClientBase</code>. Below is an abbreviated version of the resulting subclass.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PetstoreHybridClientSubclass</span> : <span class="hljs-title">HybridOpenApiClientBase</span>
{
    <span class="hljs-comment">// Base overrides</span>

    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnFailure</span>(<span class="hljs-params">
        <span class="hljs-keyword">string</span> operationId,
        IEnumerable&lt;(<span class="hljs-keyword">string</span>, <span class="hljs-keyword">string</span></span>)&gt; parameters,
        JsonResponse response)</span>
    {
        LogFailure(operationId, response);
        <span class="hljs-keyword">base</span>.OnFailure(operationId, parameters, response);
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">LogFailure</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> operationId, JsonResponse response</span>)</span>
    {
        <span class="hljs-comment">// ... as before ...</span>
    }

    <span class="hljs-comment">// API methods as before...</span>

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">AddPetAsync</span>(<span class="hljs-params">
        Pet body</span>)</span>
    {
        <span class="hljs-keyword">await</span> Client.PerformAsync(
            <span class="hljs-string">"addPet"</span>,
            [<span class="hljs-meta">
                (<span class="hljs-meta-string">"body"</span>, Serialize(body)),
            </span>]);
    }
}
</code></pre>
<p>The result was less code than the composition-based version, as there was no need to implement a factory method. Another nice feature was that Visual Studio prompts for overrides, which provides guidance as to what can be customised.</p>
<h2 id="heading-summary">Summary</h2>
<p>It was an educational experience to explore creating the client class without using inheritance. In this case, I actually favoured the inheritance-based result. I guess, in this case, the relationship was more of an 'is-a' than a 'has-a'. What was interesting, was that the base class was built using the composition-based implementation. This does show the flexibility of composition.</p>
<p>What I definitely did learn, was that I should keep an open mind when designing classes and not immediately start thinking of class hierarchies. To start with composition, and then wait for the need for inheritance to emerge. As the Thoughtworks <a target="_blank" href="https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose">Composition vs. Inheritance: How to Choose?</a> article says:</p>
<blockquote>
<p>If you find that you are using a component to provide the vast majority of your functionality, creating forwarding methods on your class to call the component's methods, exposing the component's fields, etc., consider whether inheritance - for some or all of the desired behavior - might be more appropriate.</p>
</blockquote>
<p>Which is echoed by the Code Maze article <a target="_blank" href="https://code-maze.com/csharp-composition-vs-inheritance/">Composition vs Inheritance in C#</a>:</p>
<blockquote>
<p>So, we should use inheritance if:</p>
<ul>
<li>There is an "is-a" relationship between classes (X is a Y)</li>
<li>The derived class can have all the functionality of the base class</li>
</ul>
<p>For all other instances, the composition is the preferred choice.</p>
</blockquote>
<h2 id="heading-links-and-further-reading">Links and further reading</h2>
<ul>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Composition_over_inheritance">Wikipedia - Composition over inheritance</a></p>
<ul>
<li><blockquote>
<p>Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle that classes should favor polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) over inheritance from a base or parent class.</p>
</blockquote>
</li>
<li><blockquote>
<p>To favor composition over inheritance is a design principle that gives the design higher flexibility. It is more natural to build business-domain classes out of various components than trying to find commonality between them and creating a family tree. In other words, it is better to compose what an object can do (has-a) than extend what it is (is-a).</p>
</blockquote>
</li>
<li><blockquote>
<p>One common drawback of using composition instead of inheritance is that methods being provided by individual components may have to be implemented in the derived type, even if they are only forwarding methods. In contrast, inheritance does not require all of the base class's methods to be re-implemented within the derived class.</p>
<ul>
<li>C# provides default interface methods since version 8.0 which allows to define body to interface member.</li>
</ul>
</blockquote>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose">Thoughtworks - Composition vs. Inheritance: How to Choose?</a></p>
<ul>
<li><blockquote>
<p>Composition is fairly easy to understand - we can see composition in everyday life: a chair has legs, a wall is composed of bricks and mortar, and so on. While the definition of inheritance is simple, it can become a complicated, tangled thing when used unwisely. Inheritance is more of an abstraction that we can only talk about, not touch directly. Though it is possible to mimic inheritance using composition in many situations, it is often unwieldy to do so. The purpose of composition is obvious: make wholes out of parts. The purpose of inheritance is a bit more complex because inheritance serves two purposes, semantics and mechanics.</p>
</blockquote>
</li>
<li><blockquote>
<p>Inheritance captures semantics (meaning) in a classification hierarchy (a taxonomy), arranging concepts from generalized to specialized, grouping related concepts in subtrees, and so on. Inheritance captures mechanics by encoding the representation of the data (fields) and behavior (methods) of a class and making it available for reuse and augmentation in subclasses. Mechanically, the subclass will inherit the implementation of the superclass and thus also its interface.</p>
</blockquote>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://code-maze.com/csharp-composition-vs-inheritance/">Code Maze - Composition vs Inheritance in C#</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Using Microsoft.OpenApiReader to create a dynamic REST API client]]></title><description><![CDATA[In my previous post, I discovered the Microsoft.OpenApi packages and used them to extract JSON schemas from an Open API document.
At the end of that post, I pondered if it would be possible to build on my experience to create a client that could be u...]]></description><link>https://www.10printiamcool.com/using-microsoftopenapireader-to-create-a-dynamic-rest-api-client</link><guid isPermaLink="true">https://www.10printiamcool.com/using-microsoftopenapireader-to-create-a-dynamic-rest-api-client</guid><category><![CDATA[C#]]></category><category><![CDATA[swagger]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 22 Sep 2024 08:46:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/G-HRuwCTR7c/upload/f190e47f59370f3e50f9a53dfa86b37f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a target="_blank" href="https://www.10printiamcool.com/validating-json-requests-using-c-and-openapiswagger">previous post</a>, I discovered the <a target="_blank" href="https://github.com/microsoft/OpenAPI.NET">Microsoft.OpenApi</a> packages and used them to extract JSON schemas from an Open API document.</p>
<p>At the end of that post, I pondered if it would be possible to build on my experience to create a client that could be used as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> petStoreClient =
    <span class="hljs-keyword">await</span> OpenApiClient.CreateAsync(
        File.ReadAllText(<span class="hljs-string">"petstore.swagger.json"</span>), <span class="hljs-string">"https://petstore.swagger.io"</span>);

<span class="hljs-keyword">var</span> getPetByIdResponse =
    <span class="hljs-keyword">await</span> petStoreClient.PerformAsync(<span class="hljs-string">"getPetById"</span>, [(<span class="hljs-string">"petId"</span>, <span class="hljs-string">"0"</span>)]);
</code></pre>
<p>Now, there are many good options for creating static clients. According to <a target="_blank" href="https://claude.ai">Claude.ai</a>, these include:</p>
<ol>
<li><p>Swagger Codegen</p>
<ul>
<li>An open-source tool that can generate client SDKs in various languages, including C#.</li>
</ul>
</li>
<li><p>OpenAPI Generator</p>
<ul>
<li>A fork of Swagger Codegen with additional features and improvements.</li>
</ul>
</li>
<li><p>NSwag</p>
<ul>
<li>A .NET/TypeScript toolchain for OpenAPI.</li>
</ul>
</li>
<li><p>Kiota</p>
<ul>
<li>Microsoft's latest API client generator, designed to be lightweight and adaptable.</li>
</ul>
</li>
<li><p>SwaggerHub</p>
<ul>
<li>Offers code generation capabilities, including C# clients.</li>
</ul>
</li>
</ol>
<p>Claude.ai also reminded me that Visual Studio Connected Services is built into Visual Studio, and it can generate C# clients from OpenAPI specifications.</p>
<p>Although I would probably go down the static route for a production system, I was still intrigued by the idea of having a single class that I could configure dynamically configure with just the OpenAPI document. Given this I decided to press on.</p>
<h2 id="heading-how-did-i-get-on">How did I get on?</h2>
<p>In short, I succeeded. The result is the <code>OpenApiClientV2</code> class that can be found in GitHub <a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/OpenApiDynamicClient/OpenApiDynamicClient/OpenApiClientV2.cs">here</a>.</p>
<p>Example usage can be seen below:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> petStoreClient =
    <span class="hljs-keyword">await</span> OpenApiClientV2.CreateAsync(
        File.ReadAllText(<span class="hljs-string">"petstore.swagger.json"</span>),
        <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">"https://petstore.swagger.io"</span>));

<span class="hljs-keyword">var</span> getPetByIdResponse =
    <span class="hljs-keyword">await</span> petStoreClient.PerformAsync(<span class="hljs-string">"getPetById"</span>, [(<span class="hljs-string">"petId"</span>, <span class="hljs-string">"2"</span>)]);

<span class="hljs-keyword">if</span> (getPetByIdResponse.IsSuccessful)
    Console.WriteLine(getPetByIdResponse.Payload);
</code></pre>
<p>In addition to using <a target="_blank" href="https://github.com/RicoSuter/NJsonSchema">NJsonSchema</a> to validate the request bodies, I used the popular <a target="_blank" href="https://restsharp.dev/">RestSharp</a> package to make the HTTP calls. The main routine panned out as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task&lt;JsonResponse&gt; <span class="hljs-title">PerformClientOperationAsync</span>(<span class="hljs-params">
    ClientOperation clientOperation,
    IEnumerable&lt;(<span class="hljs-keyword">string</span>, <span class="hljs-keyword">string</span></span>)&gt; parameters)</span>
{
    <span class="hljs-keyword">var</span> restRequest =
        <span class="hljs-keyword">new</span> RestRequest(
            clientOperation.Path, GetMethod(clientOperation.OperationType));

    <span class="hljs-keyword">var</span> parameterErrors = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">string</span>&gt;();

    SetNonBodyParameters(clientOperation, parameters, restRequest, parameterErrors);

    SetBodyParameter(clientOperation, parameters, restRequest, parameterErrors);

    <span class="hljs-keyword">if</span> (parameterErrors.Count &gt; <span class="hljs-number">0</span>)
    {
        <span class="hljs-keyword">return</span> GetJsonResponse(parameterErrors);
    }

    <span class="hljs-keyword">var</span> restResponse = <span class="hljs-keyword">await</span> _restClient.ExecuteAsync(restRequest);

    <span class="hljs-keyword">var</span> jsonResponse = GetJsonResponse(clientOperation, restResponse);

    <span class="hljs-keyword">return</span> jsonResponse;
}
</code></pre>
<p>By the time this method is called, the OpenAPI document had been pre-processed. In the factory method, the OpenAPI document is read, checked for errors, then turned into a dictionary of <code>ClientOperation</code> instances. The <code>ClientOperation</code> class encapsulates the details for a particular operation, as defined in the OpenAPI document.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;OpenApiClientV2&gt; <span class="hljs-title">CreateAsync</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> openApiJson, Uri domainUri</span>)</span>
{
    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> openApiJsonStream =
        <span class="hljs-keyword">new</span> MemoryStream(Encoding.UTF8.GetBytes(openApiJson));

    <span class="hljs-keyword">var</span> openApiDocument =
        <span class="hljs-keyword">new</span> OpenApiStreamReader().Read(openApiJsonStream, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> openApiDiagnostic);

    AssertNoOpenApiErrors(openApiDiagnostic);

    <span class="hljs-keyword">var</span> clientOperations = <span class="hljs-keyword">await</span> BuildClientOperationsAsync(openApiDocument);

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> OpenApiClientV2(clientOperations, baseUri);
}
</code></pre>
<p>This processing allows an efficient use of the operation details to validate the body and non-body parameters. For example, the JSON schemas for request bodies are generated at the point, to be reused as long as the client instance is held.</p>
<p>The <code>RestSharp</code> package greatly simplified the client development. In particular, the <code>AddUrlSegment</code> method allowed me to set the request parameters without having to worry about any string parsing or encoding.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">AddPathParameter</span>(<span class="hljs-params">
    OpenApiParameter openApiParameter,
    IEnumerable&lt;<span class="hljs-keyword">string</span>&gt; parameterValues,
    RestRequest restRequest,
    List&lt;<span class="hljs-keyword">string</span>&gt; parameterErrors</span>)</span>
{
    <span class="hljs-keyword">if</span> (parameterValues.Count() &gt; <span class="hljs-number">1</span>)
    {
        parameterErrors.Add(
            <span class="hljs-string">$"<span class="hljs-subst">{openApiParameter.Name}</span> path parameter has multiple values"</span>);
        <span class="hljs-keyword">return</span>;
    }

    restRequest.AddUrlSegment(openApiParameter.Name, parameterValues.First());
}
</code></pre>
<p>It was a similar situation for the other types of parameters. I could write my code without worrying about encodings, so keeping it nice and clean.</p>
<pre><code class="lang-csharp">restRequest.AddQueryParameter(openApiParameter.Name, parameterValue);
restRequest.AddHeader(openApiParameter.Name, parameterValues.First());
</code></pre>
<p>I do confess to only going so far with validating the non-body parameters. My code does check string lengths and apply the supplied regular expression, if available. However, I did not implement numerical limit checks or support for mixed types as mentioned in the <a target="_blank" href="https://swagger.io/docs/specification/data-models/data-types/">Swagger data types specification</a>.</p>
<p>I also left placeholders for extension points. These would allow customisation of the headers supplied for each call. The idea being that this would allow the appropriate authorisation headers to be set for each call.</p>
<p>One thing that did become apparent during development, was that the <code>OpenApiDocument</code> implementation does not contain all possible OpenAPI properties. For example, the <code>basePath</code> property is specified in the Petstore example:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/swagger-with-baseurl-highlighted.png?raw=true" alt="Swagger definition with baseUrl highlighted" /></p>
<p>However, when inspecting the <code>OpenApiDocument</code> instance, it was nowhere to be seen:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/quick-watch-showing-no-baseurl-in-openapi-document.png?raw=true" alt="Quick Watch showing no baseUrl in OpenApiDocument" /></p>
<p>As a result, I had to add a <code>SelectBasePath</code> method that parsed the OpenAPI document JSON and extracted the value.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;OpenApiClientV2&gt; <span class="hljs-title">CreateAsync</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> openApiJson, Uri domainUri</span>)</span>
{
    <span class="hljs-comment">// &lt;snip&gt;</span>

    <span class="hljs-keyword">var</span> basePath = SelectBasePath(openApiJson); <span class="hljs-comment">// basePath not in OpenApiDocument</span>
    <span class="hljs-keyword">var</span> baseUri = <span class="hljs-keyword">new</span> Uri(domainUri, basePath);

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> OpenApiClientV2(clientOperations, baseUri);
}
</code></pre>
<p>This wasn't a big deal, but is something to be aware of if you are using <code>OpenApiDocument</code>. Another example is <code>collectionFormat</code>, which specifies how a collection of parameters is packaged.</p>
<p>Overall, I was quite pleased with the final result and felt it had quite a bit of promise.</p>
<h2 id="heading-comparing-with-a-statically-generated-client">Comparing with a statically-generated client</h2>
<p>I thought it would be interesting to compare my dynamic client with a statically-generated client. To do this, I thought I would use the built in functionality in Visual Studio.</p>
<p>This is done by right-clicking on a project and adding a connected service.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/adding-a-connected-service.png?raw=true" alt="Adding a connected service in Visual Studio" /></p>
<p>The next step is to select the type of connected service. This will depending on the type of your Visual Studio project. I was working with a .NET Framework project, so I only got the option for OpenAPI.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/connected-service-list.png?raw=true" alt="Connected service list" /></p>
<p>For .NET Core projects, I believe you get the option of <a target="_blank" href="https://grpc.io/">gRPC</a> and perhaps others.</p>
<p>Adding an OpenAPI service is as simple as pointing the wizard to the OpenAPI document and providing the namespace, class name, and language of your choice.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/adding-a-new-openapi-service.png?raw=true" alt="Adding a new OpenAPI service" /></p>
<p>After the wizard runs, we see a single <code>.cs</code> file containing multiple classes for the API client, the API models, and other sundries.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/generated-petstore-classes.png?raw=true" alt="Generated petstore classes" /></p>
<p>The wizard adds a number of NuGet packages, but still doesn't compile. For some reason, it fails to add the <code>System.ComponentModel.DataAnnotations</code> package.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/data-annotations-missing.png?raw=true" alt="Data annotations missing" /></p>
<p>Adding this package was all that was required to get the code compiling and ready for use, an example of which is shown below.</p>
<pre><code class="lang-csharp">HttpClient httpClient = <span class="hljs-keyword">new</span>();

PetstoreClient petstoreClient = <span class="hljs-keyword">new</span>(httpClient);

Pet getPetByIdResponse = <span class="hljs-keyword">await</span> petstoreClient.GetPetByIdAsync(<span class="hljs-number">2</span>);
</code></pre>
<p>I am generally a fan of strong-typing, so this usage does appeal to me.</p>
<h2 id="heading-comparing-and-contrasting-the-two-approaches">Comparing and contrasting the two approaches</h2>
<h3 id="heading-success-flag-vs-exceptions">Success Flag vs Exceptions</h3>
<p>The dynamic client catches all failures and returns an envelope class with a <code>IsSuccessful</code> flag. This includes all parameter validation errors, non-success HTTP status codes, and any exceptions. This provides consistency for the calling code, making the code cleaner.</p>
<p>Depending of the failure mode, the static client throws a variety of exceptions. For example, if you supply an invalid request body then you get a <code>JsonSerializationException</code> thrown. If the domain is incorrect, you get a <code>WebException</code>, and if you get a non-success HTTP status code then an <code>ApiException</code> is thrown. There may be others that I did not find. This does mean that the calling code has to be aware of all of these, if it wants to make the most of them when handling them.</p>
<p>I do like consistency, so here I favoured the approach taken by my dynamic client.</p>
<h3 id="heading-runtime-checking-vs-compile-time-parameter-checking">Runtime checking vs compile-time parameter checking</h3>
<p>One clear difference between the two approaches is in the type checking. The dynamic client fits the scenario I had, where the calling code was generating JSON. However, in general, I would favour leaning on the compiler to verify types wherever I can. With this in mind, as a user, I would prefer the static client.</p>
<h3 id="heading-code-ownership">Code ownership</h3>
<p>It was interesting to have a look at the generated code. Below is a snippet from one of the implemented operations. As you can see, there is quite a bit of code and this is largely repeated in each method.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/snippet-of-auto-generated-client-code.png?raw=true" alt="Snippet of auto-generated client code" /></p>
<p>I have underlined in green the handling of non-success HTTP status codes, which - as mentioned above - results in <code>ApiException</code> instances being thrown.</p>
<p>I have also underlined, this time in red, some of the extension points that are available to you. The client is generated as a <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods">partial class</a>. This allows you to create your own partial class to provides your own custom implementations for these methods. This allows the generated client class to be regenerated at any time and also avoids using inheritance to provide the extension points.</p>
<p>What struck me about the generate code, was that there was quite a bit of it, and I would have to own it all if it was part of my project. I am not sure whether I would be overly comfortable with that. On the other hand, the dynamic client has much less code. Once the single class has been tested thoroughly, I would feel happier to use that rather than lots of generated code.</p>
<h2 id="heading-summary">Summary</h2>
<p>The combination of the <code>Microsoft.OpenApi</code>, <code>NJsonSchema</code>, and <code>RestSharp</code> packages made is straightforward to implement my vision of a dynamic client that could be used against any REST endpoint with a supporting OpenAPI document. Using this client would provide a consistent way for a codebase to interact with these services.</p>
<p>The comparison with a static client highlighted that you have to own the generated code and its inconsistencies. In the case shown, the exception throwing. However, there is definitely advantages to having strong typing for compile-time checking. With this in mind, I wondered if I could make a hybrid client. Something that could use the automatically-generated models, but would use the dynamic client internally and would have a usage as follows:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> client =
    <span class="hljs-keyword">await</span> PetstoreHybridOpenApiClient.CreateAsync(
        <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">"http://petstore.swagger.io"</span>));

<span class="hljs-keyword">await</span> client.AddPetAsync(<span class="hljs-keyword">new</span> Pet { Name = <span class="hljs-string">"Luna"</span> });

Pet pet = <span class="hljs-keyword">await</span> client.GetPetByIdAsync(<span class="hljs-number">2999</span>);

ICollection&lt;Pet&gt; pets = <span class="hljs-keyword">await</span> client.FindPetsByStatusAsync([Anonymous.Sold]);
</code></pre>
<p>One for another post perhaps.</p>
]]></content:encoded></item><item><title><![CDATA[Validating JSON Requests Using C# and OpenAPI/Swagger]]></title><description><![CDATA[Recently, I needed to integrate an internal system that generates JSON with a third-party API. Usefully, I had an OpenAPI specification for the API in question. OpenAPI is a specification for machine-readable interface files for describing, producing...]]></description><link>https://www.10printiamcool.com/validating-json-requests-using-c-and-openapiswagger</link><guid isPermaLink="true">https://www.10printiamcool.com/validating-json-requests-using-c-and-openapiswagger</guid><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[swagger]]></category><category><![CDATA[OpenApi]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Mon, 19 Aug 2024 17:48:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/gMJ3tFOLvnA/upload/91778a14036ace92d5a0ab69ed40174f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I needed to integrate an internal system that generates JSON with a third-party API. Usefully, I had an OpenAPI specification for the API in question. <a target="_blank" href="https://swagger.io/specification/">OpenAPI</a> is a specification for machine-readable interface files for describing, producing, consuming, and visualising RESTful web services. It is also well-known as its previous name of Swagger. You can see and interact with an example specification via the online <a target="_blank" href="https://editor.swagger.io/">Swagger Editor</a>.</p>
<p>The specification includes definitions of the objects used by the API. In the <a target="_blank" href="https://petstore.swagger.io/">Petstore</a> example, the method to add a pet has a payload that takes a <code>Pet</code> object as a its payload.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Pet:</span>
  <span class="hljs-attr">required:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">name</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">photoUrls</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">object</span>
  <span class="hljs-attr">properties:</span>
    <span class="hljs-attr">id:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">integer</span>
      <span class="hljs-attr">format:</span> <span class="hljs-string">int64</span>
    <span class="hljs-attr">name:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
    <span class="hljs-attr">category:</span>
      <span class="hljs-string">$ref:</span> <span class="hljs-string">"#/components/schemas/Category"</span>
    <span class="hljs-attr">photoUrls:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">array</span>
      <span class="hljs-attr">items:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
</code></pre>
<p>What I wanted to do, was use this information to validate the JSON being generated by the internal system before it was sent to the external API. The question was how?</p>
<h2 id="heading-the-road-to-the-microsoftopenapi-library">The road to the Microsoft.OpenApi library</h2>
<p>My first thought was to manually parse the OpenAPI specification. So I asked <a target="_blank" href="https://claude.ai">Claude.ai</a>:</p>
<blockquote>
<p>Please generate a C# method that takes a Swagger definition in JSON and an operation id and returns a JSON schema for the operation request body.</p>
</blockquote>
<p>Claude obliged, but the resulting code was far from promising.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> JsonDocument doc = JsonDocument.Parse(swaggerJson);
  JsonElement root = doc.RootElement;

  <span class="hljs-comment">// Iterate through paths</span>
  <span class="hljs-keyword">foreach</span> (JsonProperty pathProp <span class="hljs-keyword">in</span> root.GetProperty(<span class="hljs-string">"paths"</span>).EnumerateObject())
  {
      <span class="hljs-keyword">foreach</span> (JsonProperty methodProp <span class="hljs-keyword">in</span> pathProp.Value.EnumerateObject())
      {
          <span class="hljs-keyword">if</span> (methodProp.Value.TryGetProperty(<span class="hljs-string">"operationId"</span>, <span class="hljs-keyword">out</span> JsonElement opIdElement)
              &amp;&amp; opIdElement.GetString() == operationId)
          {
              <span class="hljs-comment">// Found the operation, now extract the request body schema</span>
              <span class="hljs-keyword">if</span> (methodProp.Value.TryGetProperty(<span class="hljs-string">"requestBody"</span>, <span class="hljs-keyword">out</span> JsonElement requestBody)
                  &amp;&amp; requestBody.TryGetProperty(<span class="hljs-string">"content"</span>, <span class="hljs-keyword">out</span> JsonElement content))
              {
                  <span class="hljs-keyword">foreach</span> (JsonProperty contentType <span class="hljs-keyword">in</span> content.EnumerateObject())
                  {
                      <span class="hljs-keyword">if</span> (contentType.Value.TryGetProperty(<span class="hljs-string">"schema"</span>, <span class="hljs-keyword">out</span> JsonElement schema))
                      {
</code></pre>
<p>It occurred to me that my prompt was not high-level enough. I was assuming a certain solution. So instead, I tried the following request.</p>
<blockquote>
<p>I am using .NET Framework and C#. I have a Swagger definition for an API. How can I extract the JSON Schema for the request and response body for each operation in the definition?</p>
</blockquote>
<p>And the response led me to the Microsoft.OpenApi library.</p>
<blockquote>
<p>To extract the JSON Schema for request and response bodies from a Swagger definition in a .NET Framework and C# environment, you can use the Microsoft.OpenApi library. This library provides tools to parse and manipulate OpenAPI (formerly known as Swagger) documents.</p>
</blockquote>
<p>As I could see from the generated code, this looked far more like it.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> openApiDocument =
    <span class="hljs-keyword">new</span> OpenApiStreamReader().Read(
        File.OpenRead(swaggerFilePath), <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> diagnostic);

<span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> path <span class="hljs-keyword">in</span> openApiDocument.Paths)
{
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> operation <span class="hljs-keyword">in</span> path.Value.Operations)
    {
        <span class="hljs-keyword">var</span> operationType = operation.Key.ToString();
        <span class="hljs-keyword">var</span> operationId = operation.Value.OperationId;

        <span class="hljs-comment">// Extract request body schema</span>
        <span class="hljs-keyword">if</span> (operation.Value.RequestBody != <span class="hljs-literal">null</span> &amp;&amp;
            operation.Value.RequestBody.Content.TryGetValue(
                <span class="hljs-string">"application/json"</span>, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> requestMediaType))
        {
            <span class="hljs-keyword">var</span> requestSchema = requestMediaType.Schema;
</code></pre>
<h2 id="heading-getting-claudeais-code-to-work">Getting Claude.ai's code to work</h2>
<p>This was great, but it only had one flaw. It didn't work. When I tried running the code I got the following exception.</p>
<pre><code class="lang-text">Newtonsoft.Json.JsonSerializationException
  HResult=0x80131500
  Message=Self referencing loop detected for property 'HostDocument' with type 'Microsoft.OpenApi.Models.OpenApiDocument'. Path 'Properties.category.Reference.HostDocument.Paths['/pet/{petId}/uploadImage'].Operations.Post.Tags[0].Reference'.
  Source=Newtonsoft.Json
</code></pre>
<p>The exceptioin occurred on the following line.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> schemaData = JsonConvert.SerializeObject(openApiSchema);
</code></pre>
<p>Claude.ai had predicted that something called <code>OpenApiSchema</code> was a serializable JSON schema. It turns out that it isn't, so I had to go back to some old-fashioned searching on <a target="_blank" href="https://stackoverflow.com/">StackOverflow</a>. My search turned up the following question, <a target="_blank" href="https://stackoverflow.com/questions/71960630/get-the-json-schemas-from-a-large-openapi-document-or-using-newtonsoft-and-reso">Get the JSON Schema's from a large OpenAPI Document OR using NewtonSoft and resolve refs</a>. The accepted answer had the following code (slightly abbreviated for this post).</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> reader = <span class="hljs-keyword">new</span> OpenApiStreamReader();
<span class="hljs-keyword">var</span> result =
    <span class="hljs-keyword">await</span> reader.ReadAsync(<span class="hljs-keyword">new</span> FileStream(file.FullName, FileMode.Open));

<span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> schemaEntry <span class="hljs-keyword">in</span> result.OpenApiDocument.Components.Schemas)
{
    <span class="hljs-keyword">var</span> schemaFileName = schemaEntry.Key + <span class="hljs-string">".json"</span>;
    <span class="hljs-keyword">var</span> outputPath =
        Path.Combine(outputDir, schemaFileName + <span class="hljs-string">"-Schema.json"</span>);

    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> fileStream = <span class="hljs-keyword">new</span> FileStream(outputPath, FileMode.CreateNew);
    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> writer = <span class="hljs-keyword">new</span> StreamWriter(fileStream);

    <span class="hljs-keyword">var</span> writerSettings =
        <span class="hljs-keyword">new</span> OpenApiWriterSettings()
        {
            InlineLocalReferences = <span class="hljs-literal">true</span>,
            InlineExternalReferences = <span class="hljs-literal">true</span>
        };

    schemaEntry.Value
        .SerializeAsV2WithoutReference(
            <span class="hljs-keyword">new</span> OpenApiJsonWriter(writer, writerSettings));
}
</code></pre>
<p>Sure enough, when I ran this code against the Petstore OpenAPI document, it successfully created the following schema files:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/openapi-posts/exported-schema-file-list.png?raw=true" alt="List of exported JSON schemas" /></p>
<p>Opening up <code>User.json-Schema.json</code>, I could see that the contents looked to my eye like a valid JSON schema.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"object"</span>,
  <span class="hljs-attr">"properties"</span>: {
    <span class="hljs-attr">"id"</span>: {
      <span class="hljs-attr">"format"</span>: <span class="hljs-string">"int64"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"integer"</span>
    },
    <span class="hljs-attr">"petId"</span>: {
      <span class="hljs-attr">"format"</span>: <span class="hljs-string">"int64"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"integer"</span>
    },
    <span class="hljs-attr">"quantity"</span>: {
      <span class="hljs-attr">"format"</span>: <span class="hljs-string">"int32"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"integer"</span>
    },
    <span class="hljs-attr">"shipDate"</span>: {
      <span class="hljs-attr">"format"</span>: <span class="hljs-string">"date-time"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>
    },
    <span class="hljs-attr">"status"</span>: {
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Order Status"</span>,
      <span class="hljs-attr">"enum"</span>: [<span class="hljs-string">"placed"</span>, <span class="hljs-string">"approved"</span>, <span class="hljs-string">"delivered"</span>],
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>
    },
    <span class="hljs-attr">"complete"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"boolean"</span>
    }
  },
  <span class="hljs-attr">"xml"</span>: {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Order"</span>
  }
}
</code></pre>
<p>The key here was the use of the <code>SerializeAsV2WithoutReference</code> method with the <code>OpenApiJsonWriter</code> and <code>OpenApiWriterSettings</code>. Together they control how an <code>OpenApiSchema</code> instance is serialized. In this case, we want to inline all references to get a self-contained schema.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> writerSettings =
    <span class="hljs-keyword">new</span> OpenApiWriterSettings()
    {
        InlineLocalReferences = <span class="hljs-literal">true</span>,
        InlineExternalReferences = <span class="hljs-literal">true</span>
    };

schemaEntry.Value
    .SerializeAsV2WithoutReference(
        <span class="hljs-keyword">new</span> OpenApiJsonWriter(writer, writerSettings));
</code></pre>
<p>Now I had a way of extracting the schemas, I could move on to my ultimate aim of using the schemas to validate my dynamically-created requests.</p>
<h2 id="heading-creating-my-openapischemavalidator">Creating my OpenApiSchemaValidator</h2>
<p>As I mentioned in my <a target="_blank" href="https://www.10printiamcool.com/designing-a-cdk-state-machine-builder-part-1">Designing a CDK State Machine Builder</a> post, I like to build software using an API-first approach. That is, before jumping into the component implementation, I imagine it exists and write the code the calling code. This allows me to quickly iterate over the external API until it feels it has the right level of expression and abstraction.</p>
<p>With this in mind, I imagined a <code>OpenApiSchemaValidator</code> class and then iterated on the calling code until I got the following.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> validator =
    <span class="hljs-keyword">new</span> OpenApiSchemaValidator(
        <span class="hljs-keyword">new</span> FileStream(<span class="hljs-string">"petstore.swagger.json"</span>, FileMode.Open));

<span class="hljs-keyword">var</span> validationResult =
    validator.ValidateRequestBodyJson(
        operationId: <span class="hljs-string">"addPet"</span>, bodyJson: petJson);

<span class="hljs-keyword">if</span> (validationResult.IsValid)
    Console.WriteLine(<span class="hljs-string">"Request JSON is valid"</span>);
<span class="hljs-keyword">else</span>
    Console.WriteLine(
        <span class="hljs-string">$"Request JSON had the following errors: \n- "</span> +
        <span class="hljs-string">$"<span class="hljs-subst">{<span class="hljs-keyword">string</span>.Join(<span class="hljs-string">"\n- "</span>, validationResult.Errors)}</span>"</span>);
</code></pre>
<p>I decided to split the creation of <code>OpenApiSchemaValidator</code> from the method call. This would allow the creation of the instance to process the OpenAPI document and avoid this static overhead on each validation call.</p>
<p>However, when I dived int the implementation, I had to make a small change. I was using the <a target="_blank" href="https://github.com/RicoSuter/NJsonSchema?tab=readme-ov-file#njsonschema-for-net">NJsonSchema for .NET</a> NuGet package to validate the JSON requests against the JSON schemas. It turned out that the method to parse the JSON schema was <code>async</code>. Since I could not have this in a constructor, I had to have an <code>async</code> factory method instead.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> validator =
    <span class="hljs-keyword">await</span> OpenApiSchemaValidator.CreateAsync(
        <span class="hljs-keyword">new</span> FileStream(<span class="hljs-string">"petstore.swagger.json"</span>, FileMode.Open));
</code></pre>
<p>The implementation is shown below.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IReadOnlyDictionary&lt;<span class="hljs-keyword">string</span>, JsonSchema&gt; _requestBodyJsonSchemas;

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-title">OpenApiSchemaValidator</span>(<span class="hljs-params">IDictionary&lt;<span class="hljs-keyword">string</span>, JsonSchema&gt; jsonSchemas</span>)</span> =&gt;
    _requestBodyJsonSchemas = <span class="hljs-keyword">new</span> Dictionary&lt;<span class="hljs-keyword">string</span>, JsonSchema&gt;(jsonSchemas);

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;OpenApiSchemaValidator&gt; <span class="hljs-title">CreateAsync</span>(<span class="hljs-params">Stream openApiStream</span>)</span>
{
    <span class="hljs-comment">// Get the operations with JSON request bodies</span>

    <span class="hljs-keyword">var</span> openApiDocument =
        <span class="hljs-keyword">new</span> OpenApiStreamReader().Read(openApiStream, <span class="hljs-keyword">out</span> _);

    <span class="hljs-keyword">var</span> requestBodyOperations =
        openApiDocument.Paths
            .SelectMany(p =&gt; p.Value.Operations)
            .Where(o =&gt;
                o.Value.RequestBody != <span class="hljs-literal">null</span> &amp;&amp;
                o.Value.RequestBody.Content.ContainsKey(<span class="hljs-string">"application/json"</span>));

    <span class="hljs-comment">// Convert the OpenAPI schemas to JSON schemas and index by Operation Id</span>

    <span class="hljs-keyword">var</span> jsonSchemas = <span class="hljs-keyword">new</span> Dictionary&lt;<span class="hljs-keyword">string</span>, JsonSchema&gt;();

    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> operation <span class="hljs-keyword">in</span> requestBodyOperations)
    {
        <span class="hljs-keyword">var</span> openApiSchema =
            operation.Value.RequestBody.Content[<span class="hljs-string">"application/json"</span>].Schema;
        <span class="hljs-keyword">var</span> jsonSchema =
            <span class="hljs-keyword">await</span> JsonSchema.FromJsonAsync(SerializeToJsonSchema(openApiSchema));
        jsonSchemas.Add(operation.Value.OperationId, jsonSchema);
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span>(jsonSchemas);
}
</code></pre>
<p>The <code>SerializeToJsonSchema</code> method used the <code>SerializeAsV2WithoutReference</code> method as discussed earlier.</p>
<p>With the dictionary of schemas in place, applying them was straightforward.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> OpenApiSchemaValidationResult <span class="hljs-title">ValidateRequestBodyJson</span>(<span class="hljs-params">
    <span class="hljs-keyword">string</span> operationId,
    <span class="hljs-keyword">string</span> bodyJson</span>)</span>
{
    <span class="hljs-keyword">if</span> (_requestBodyJsonSchemas.TryGetValue(operationId, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> jsonSchema))
    {
        <span class="hljs-comment">// Validate the JSON against the schema</span>

        <span class="hljs-keyword">var</span> errors = jsonSchema.Validate(JToken.Parse(bodyJson));

        <span class="hljs-keyword">return</span>
            <span class="hljs-keyword">new</span> OpenApiSchemaValidationResult
            {
                IsValid = errors.Count == <span class="hljs-number">0</span>,
                Errors = errors.Select(e =&gt; <span class="hljs-string">$"<span class="hljs-subst">{e.Path}</span>: <span class="hljs-subst">{e.Kind}</span>"</span>)
            };
    }

    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentException(
        <span class="hljs-string">$"Operation does not have a JSON request body: <span class="hljs-subst">{operationId}</span>"</span>,
        <span class="hljs-keyword">nameof</span>(operationId));
}
</code></pre>
<p>And with that in place, I could test the validation with an empty JSON object.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> validationResult =
    validator.ValidateRequestBodyJson(operationId: <span class="hljs-string">"addPet"</span>, bodyJson: <span class="hljs-string">"{}"</span>);
</code></pre>
<p>Which resulted in the following output to be written to the console, successfully reporting that the JSON request had two missing properties.</p>
<pre><code class="lang-text">Request JSON had the following errors:
- #/name: PropertyRequired
- #/photoUrls: PropertyRequired
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>Thanks to Claude.ai and StackOverflow, I was able to implement the functionality I wanted, in a class only 95 lines long. The key piece of information was the existence of the <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.openapi">Microsoft.OpenApi namespace</a>. This did all the heavy lifting of reading the OpenApi document and outputting the request schemas as JSON schemas. From there, it was straightforward to use <a target="_blank" href="https://github.com/RicoSuter/NJsonSchema?tab=readme-ov-file#njsonschema-for-net">NJsonSchema</a> to do the validation. The resulting OpenApiSchemaValidator class can be found on GitHub <a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/OpenApiDynamicClient/ConsoleScratchpad/OpenApiSchemaValidator.cs">here</a>.</p>
<p>Now that I had a way of reading the OpenAPI document, I realised that I could do more with it than just validating request bodies. An OpenAPI document includes the paths, parameters, and more for each endpoint. I asked myself, would it be possible to build a dynamic OpenAPI client that I could use as follows?</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> petStoreClient =
    <span class="hljs-keyword">await</span> OpenApiClient.CreateAsync(
        <span class="hljs-keyword">new</span> FileStream(<span class="hljs-string">"petstore.swagger.json"</span>, FileMode.Open),
        <span class="hljs-string">"https://petstore.swagger.io/v2"</span>);

<span class="hljs-keyword">var</span> getPetByIdResponse =
    <span class="hljs-keyword">await</span> petStoreClient.PerformAsync(<span class="hljs-string">"getPetById"</span>, [(<span class="hljs-string">"petId"</span>, <span class="hljs-string">"0"</span>)]);
</code></pre>
<p>One for another post I think.</p>
]]></content:encoded></item><item><title><![CDATA[Connecting to cloud Azure Blob Storage]]></title><description><![CDATA[In the previous post in this series on creating a serverless webhook proxy, I used Azurite (local Blob Storage emulator) to develop the code that stores the requests received. This was very convenient, but avoided a number of concerns that we encount...]]></description><link>https://www.10printiamcool.com/connecting-to-cloud-azure-blob-storage</link><guid isPermaLink="true">https://www.10printiamcool.com/connecting-to-cloud-azure-blob-storage</guid><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[azure-blobstorage]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 23 Jun 2024 18:43:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7BhTfoKsheQ/upload/195954fa8dce000ae4bab091667efcc9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous <a target="_blank" href="https://www.10printiamcool.com/using-local-blob-storage">post</a> in this <a target="_blank" href="https://www.10printiamcool.com/series/azure-serverless">series</a> on creating a serverless webhook proxy, I used <a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage">Azurite</a> (local Blob Storage emulator) to develop the code that stores the requests received. This was very convenient, but avoided a number of concerns that we encounter when we want to use cloud-based resources. These concerns relate to access, security, debugging, and others.</p>
<h2 id="heading-connection-strings-and-account-keys-are-evil">Connection strings and account keys are evil</h2>
<p>'Evil' is perhaps a strong word, but both are risky as they can easily be leaked. I have seen too many credentials shared in plain text in emails or chats. There have also been too many stories of credentials being accidentally checked into source control. So, what is the best way to handle them? The answer is, not to have them in the first place.</p>
<p>I am familiar with the AWS serverless offerings, Lambda functions, DynamoDB, SQS, and so forth. In all these cases, I have never had to use a connection string or an account key. In AWS, every component needs to be granted access to the resources that it needs interact with. This is done through the AWS IAM (Identity and Access Management). This avoids the use of explicit credentials and the risk associated with them. It turns out that Azure is going the same way, but first we need to create our Blob Storage containers.</p>
<h2 id="heading-creating-the-storage-account">Creating the storage account</h2>
<p>In future, I want to deploy all the cloud resources via infrastructure as code (IaC) using <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/">Bicep</a>. However, for the first pass I used click-ops and the Azure portal. From within my project resource group, I clicked to add a service and selected the 'Storage account' service from Microsoft.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/070-select-storage-account.png?raw=true" alt="Selecting storage account in Azure Portal" /></p>
<p>Next, I specified the basic properties of the account. I provided the account name and changed the 'Redundancy' level to the cheapest option, but otherwise left the properties at their default values.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/080-storage-account-basics.png?raw=true" alt="Configuring storage account basics in Azure Portal" /></p>
<p>For the networking options, I left the access level at 'Enable public access from all networks'. I experimented with a more restrictive access level, as I wasn't comfortable with the public access. However, I was not able to access the account unless this level was selected. At this point in my journey, I did not want to start creating private networks. However, for a production system, this is the route I would go.</p>
<p>Here was a significant difference to my experience with AWS, where the default for S3 buckets is to prevent public access. You can then use AWS IAM to grant bucket access to the appropriate resources, such as Lambda functions, without having to create any private networking.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/090-storage-account-network.png?raw=true" alt="Configuring storage account network in Azure Portal" /></p>
<p>Although I had no intention of using them, I took a look at the 'Access keys' blade. Here, as you would expect, you can find the access keys and connection strings to access the account. You can also manually rotate the keys from this blade, which highlights another good reason to avoid using them.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/100-storage-account-access-keys.png?raw=true" alt="Viewing storage account access keys in Azure Portal" /></p>
<h2 id="heading-creating-and-configuring-the-containers">Creating and configuring the containers</h2>
<p>With the storage account in place, I clicked the option to add a container.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/110-storage-account-container-add.png?raw=true" alt="Adding a Blob container in Azure Portal" /></p>
<p>Then specified the name of the container and accepted the other defaults.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/120-storage-account-container-details.png?raw=true" alt="Setting a Blob container details in Azure Portal" /></p>
<p>I then repeated this for the other container that I needed. The resulting list showed the two containers that I had created and one that seemed to have been created by Azure (<code>$logs</code>).</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/130-storage-account-container-list.png?raw=true" alt="Blob container list in Azure Portal" /></p>
<p>As these container are going to container personally identifiable information (PII), it is important not to hold on to the data for longer than is strictly necessary. This is where time-to-live (TTL) functionality comes in very handy. This functionality allows us to defer the responsibility of deleting old data to the cloud provider.</p>
<p>For Blob Storage, this is done by selecting 'Lifecycle management' blade, under 'Data management' in the Azure portal.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/140-storage-account-lifecycle-management.png?raw=true" alt="Lifecycle management in Azure Portal" /></p>
<p>The first step was to give the rule a name and specify the blobs to which the rule applies. In my case, I wanted to delete all request blobs after 30 days. So I selected the rule scope to filter the blobs to which the rule applies.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/150-storage-account-rule-add.png?raw=true" alt="Adding lifecycle rules in Azure Portal" /></p>
<p>The next section allowed me to specify an action to take and the condition under which to take it. In my case, I wanted to delete all blobs 30 days after creation.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/160-storage-account-rule-condition.png?raw=true" alt="Specifying lifecycle conditions in Azure Portal" /></p>
<p>The final step was to specify the filter set. In my case, I specified <code>webhook-payload-</code> to filter the rule to the request containers.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/170-storage-account-rule-filter.png?raw=true" alt="Specifying lifecycle filters in Azure Portal" /></p>
<p>Now I had my containers in place, with rules to keep them managed and, although there was public access via keys, I had no intention of using them. This is because I intended to use managed identities.</p>
<h2 id="heading-managed-identities">Managed identities</h2>
<p>The Microsoft Learn article <a target="_blank" href="https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview">What are managed identities for Azure resources?</a> give a good overview of what managed identities are.</p>
<blockquote>
<p>A common challenge for developers is the management of secrets, credentials, certificates, and keys used to secure communication between services. Managed identities eliminate the need for developers to manage these credentials.</p>
<p>While developers can securely store the secrets in Azure Key Vault, services need a way to access Azure Key Vault. Managed identities provide an automatically managed identity in Microsoft Entra ID for applications to use when connecting to resources that support Microsoft Entra authentication. Applications can use managed identities to obtain Microsoft Entra tokens without having to manage any credentials.</p>
</blockquote>
<p>It goes on to list some of echo some the benefits of using managed identities that I mentioned earlier.</p>
<blockquote>
<ul>
<li>You don't need to manage credentials. Credentials aren’t even accessible to you.</li>
<li>You can use managed identities to authenticate to any resource that supports Microsoft Entra authentication, including your own applications.</li>
<li>Managed identities can be used at no extra cost.</li>
</ul>
</blockquote>
<p>As I also mentioned, in AWS this is the default. In fact, it is the only option in order to access some services. It is good to see Azure following down this path.</p>
<p>The article goes on to mention that there are two types of managed identity, system-assigned and user-assigned. In my case, I want a system-managed identity for my Azure Function.</p>
<p>Assigning a managed identity to a function app using the Azure portal was a simple task. I selected the function app and the 'Identity' blade. Then all that was required was to switch the status to 'On'.
<img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/180-function-app-managed-identity.png?raw=true" alt="Assigning a managed identity to a function app in Azure portal" /></p>
<p>To grant access to the storage account, I needed to open the storage account and select the 'Access Control' blade. I then selected 'Add role assignment' from the 'Add' menu.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/190-storage-account-access-control.png?raw=true" alt="Storage account access control in Azure portal" /></p>
<p>The next step was to select the role that I wished to give my function app. I filtered the list to those with 'blob' in them and was given the list shown below. As the function app needs to write to the storage account, I chose 'Storage Blob Data Contributor' and to assign it to a managed identity.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/195-storage-account-add-contributor.png?raw=true" alt="Adding a contributor to a storage account in Azure portal" /></p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/200-storage-account-select-members.png?raw=true" alt="Selecting members for storage account access in Azure portal" /></p>
<p>The next step was to select my function app as a member of this role. The portal gives a dropdown list of managed identity types, so I selected 'Function App' and my function app from the resulting list.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/210-storage-account-select-function-app.png?raw=true" alt="Selecting a function app as a member in Azure portal" /></p>
<p>I then took the defaults and completed the wizard. The resulting list of role assignment confirmed that the function app identity now had access to the storage account.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/220-storage-account-access-control-list.png?raw=true" alt="Storage account access control list in Azure portal" /></p>
<p>Now all that was left was to update the code to use the managed identity to connect to the storage account.</p>
<h2 id="heading-using-token-credentials-to-connect">Using token credentials to connect</h2>
<p>The current code was connecting to the local Azurite storage emulator using a hardcoded connection string. I wanted to keep this behaviour for local testing, but also wanted to be able to connect to cloud storage when running in the cloud.</p>
<p>I tried in vain to use the <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet"><code>DefaultAzureCredential</code></a> class to seamlessly change the access mechanism depending on environment. However, despite my best efforts, I was not able to get it to work. Instead, I decided to hide the logic behind a new <code>IBlobServiceClientFactory</code> interface implementation.</p>
<p>This simple interface had a single method for creating a <code>BlobServiceClient</code>.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IBlobServiceClientFactory</span>
{
    <span class="hljs-function">BlobServiceClient <span class="hljs-title">CreateClient</span>(<span class="hljs-params"></span>)</span>;
}
</code></pre>
<p>The implementation is also quite simple. It checks an environment variable to see if the code is being run in an environment with a storage emulator. If so, the hardcoded connection string is used. If not, then a storage URI and <code>ManagedIdentityCredential</code> instance are used.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">BlobServiceClientFactory</span> : <span class="hljs-title">IBlobServiceClientFactory</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TokenCredential _tokenCredential;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">BlobServiceClientFactory</span>(<span class="hljs-params"></span>)</span>
    {
        _tokenCredential = <span class="hljs-keyword">new</span> ManagedIdentityCredential();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> BlobServiceClient <span class="hljs-title">CreateClient</span>(<span class="hljs-params"></span>)</span>
    {
        BlobServiceClient blobServiceClient;

        <span class="hljs-keyword">if</span> (Environment.GetEnvironmentVariable(
            <span class="hljs-string">"AZURE_STORAGE_EMULATOR_RUNNING"</span>) == <span class="hljs-string">"true"</span>)
        {
            <span class="hljs-comment">// Use connection string for Azurite</span>
            <span class="hljs-keyword">string</span> connectionString = <span class="hljs-string">"UseDevelopmentStorage=true"</span>;
            blobServiceClient = <span class="hljs-keyword">new</span> BlobServiceClient(connectionString);
        }
        <span class="hljs-keyword">else</span>
        {
            <span class="hljs-comment">// Use TokenCredential for Azure Storage</span>
            <span class="hljs-keyword">var</span> webhookStorageAccount =
                Environment.GetEnvironmentVariable(<span class="hljs-string">"WEBHOOK_STORAGE_ACCOUNT"</span>);
            <span class="hljs-keyword">var</span> blobServiceUri =
                <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">$"https://<span class="hljs-subst">{webhookStorageAccount}</span>.blob.core.windows.net"</span>);
            blobServiceClient =
                <span class="hljs-keyword">new</span> BlobServiceClient(blobServiceUri, _tokenCredential);
        }

        <span class="hljs-keyword">return</span> blobServiceClient;
    }
}
</code></pre>
<p>I then added this to the dependency injection configuration.</p>
<pre><code class="lang-csharp">.ConfigureServices(services =&gt;
{
   <span class="hljs-comment">// &lt;snip&gt;</span>
   services.AddSingleton&lt;IBlobServiceClientFactory, BlobServiceClientFactory&gt;();
})
</code></pre>
<p>Finally, I updated the <code>BlobPayloadStore</code> to have an instance injected and to use this instance to create an appropriate client.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> BlobServiceClient _blobServiceClient;

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">BlobPayloadStore</span>(<span class="hljs-params">IBlobServiceClientFactory blobServiceClientFactory</span>)</span>
{
   _blobServiceClient = blobServiceClientFactory.CreateClient();
}
</code></pre>
<h2 id="heading-a-special-mention-for-defaultazurecredential">A special mention for DefaultAzureCredential</h2>
<p>As mentioned in the previous section. I tried to use the <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/?tabs=command-line#defaultazurecredential"><code>DefaultAzureCredential</code></a> class. This was because the documentation states:</p>
<blockquote>
<p>The <code>DefaultAzureCredential</code> class provided by the Azure SDK allows apps to use different authentication methods depending on the environment they're run in. This allows apps to be promoted from local development to test environments to production without code changes. You configure the appropriate authentication method for each environment and <code>DefaultAzureCredential</code> will automatically detect and use that authentication method. The use of <code>DefaultAzureCredential</code> should be preferred over manually coding conditional logic or feature flags to use different authentication methods in different environments.</p>
</blockquote>
<p>This sounded exactly what I was after, but I could not find a way to get it to connect to the local emulator or the cloud storage. In the end, I had to resort to my own logic and using the <code>ManagedIdentityCredential</code> class explicitly. I am quite happy with this choice, as this means my function app can <strong>only</strong> use managed identities, which is the intention.</p>
<p>However, you may have different intentions and so it is definitely worth consideration. For more details, see the following excellent blog post: <a target="_blank" href="https://nestenius.se/2024/04/18/default-azure-credentials-under-the-hood">DefaultAzureCredentials Under the Hood</a></p>
<h2 id="heading-using-log-stream-for-quick-feedback">Using Log Stream for quick feedback</h2>
<p>When developing and debugging my function in Azure, I found the Log Stream functionality in Azure very useful. As mentioned in an <a target="_blank" href="https://www.10printiamcool.com/deploying-and-debugging-my-first-azure-function">earlier post</a>, you can attach to functions running in Azure and debug them that way. However, I find judicious log statements can be almost as good.</p>
<p>As shown below, the Log Stream blade allows an almost real-time view of the log from your function. You can filter by level, copy the output, and jump into the raw logs if you really need to.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/230-azure-function-log-stream.png?raw=true" alt="Log Stream blade in Azure portal" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, I managed (no pun intended) to achieve my desired objective of updating my Azure Function, so that it could access cloud Blob Storage without the use of connection strings or API keys. The way this was done was by assigning my Azure Function a managed identity and ensuring that identity had the appropriate role.</p>
<p>Now that we have data going into the Blob Storage, the next step is to react to the resulting events and take action based on them.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-dotnet-get-started?tabs=account-key#authorize-access-and-connect-to-blob-storage">Authorize access and connect to Blob Storage</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?tabs=azure-portal">Manage storage account access keys</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/common/authorize-data-access">Authorize access to data in Azure Storage</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/?tabs=command-line">How to authenticate .NET apps to Azure services using the .NET Azure SDK</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage">Use the Azurite emulator for local Azure Storage development</a></li>
<li><a target="_blank" href="https://nestenius.se/2024/04/18/default-azure-credentials-under-the-hood"><code>DefaultAzureCredentials</code> Under the Hood</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet?tabs=visual-studio%2Cmanaged-identity%2Croles-azure-portal%2Csign-in-azure-cli%2Cidentity-visual-studio&amp;pivots=blob-storage-quickstart-scratch">Quickstart: Azure Blob Storage client library for .NET</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Using Local Blob Storage]]></title><description><![CDATA[In the previous post in this series on creating a webhook proxy using Azure serverless technology, I added API Management in front of an Azure Function. In this post, I turn my attention to fleshing out the back-end functionality. In particular, impl...]]></description><link>https://www.10printiamcool.com/using-local-blob-storage</link><guid isPermaLink="true">https://www.10printiamcool.com/using-local-blob-storage</guid><category><![CDATA[Azure]]></category><category><![CDATA[serverless]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sat, 04 May 2024 16:12:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/JwMGy1h-JsY/upload/05e1ce37498de7c5e7045af9e3cb7f9f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://www.10printiamcool.com/exposing-an-azure-function-using-api-management">previous post</a> in this <a target="_blank" href="https://www.10printiamcool.com/series/azure-serverless">series on creating a webhook proxy</a> using Azure serverless technology, I added API Management in front of an Azure Function. In this post, I turn my attention to fleshing out the back-end functionality. In particular, implementing the 'store' part of the store and forward pattern and putting the received request payloads in Azure Blob Storage for subsequent processing.</p>
<h2 id="heading-a-quick-overview-of-azure-blob-storage">A quick overview of Azure Blob Storage</h2>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction">Blob Storage</a> is the Azure service that is equivalent to <a target="_blank" href="https://aws.amazon.com/s3/">AWS S3</a>, in that it is used for storing large amounts of unstructured data, such as text or binary data.</p>
<p>Blob Storage is composed of the following resource hierarchy:</p>
<ul>
<li><strong>Storage Account</strong>: The top-level resource, providing a globally-unique namespace in Azure for your storage data.</li>
<li><strong>Container</strong>: Provides a grouping of a set of blobs.</li>
<li><strong>Blob</strong>: The fundamental storage entity in Azure Blob Storage. There are three types of blobs:<ul>
<li><strong>Block Blobs</strong>: Used for storing text or binary files, like documents, media files, etc.</li>
<li><strong>Append Blobs</strong>: Optimized for append operations, making them ideal for scenarios like logging.</li>
<li><strong>Page Blobs</strong>: Designed for frequent read/write operations.</li>
</ul>
</li>
</ul>
<p>Access to blobs and containers is controlled through:</p>
<ul>
<li><strong>Access Keys</strong>: Storage account keys that give full privileges to the storage account.</li>
<li><strong>Shared Access Signatures (SAS)</strong>: Provides restricted access rights to containers and blobs, with a defined start time, expiry time, and permissions.</li>
<li><strong>Azure Active Directory (Azure AD)</strong>: For RBAC (role-based access control) to manage and control access.</li>
</ul>
<p>There are other aspects to Blob Storage, such as:</p>
<ul>
<li><strong>Access Tiers</strong>: To store data based on the frequency of access, such as 'Hot', 'Cool', and 'Archive'.</li>
<li><strong>Lifecycle Management</strong>: Automating the process of moving blobs to cooler storage tiers or deleting old blobs that are no longer needed.</li>
<li><strong>Security</strong>: Options regarding encryption at rest and in transit.</li>
<li><strong>Redundancy</strong>: Options such as 'Locally Redundant Storage (LRS)', 'Zone-Redundant Storage (ZRS)', and 'Geo-Redundant Storage (GRS)'.</li>
</ul>
<h2 id="heading-creating-our-containers">Creating our containers</h2>
<p>Visual Studio 2022 ships with the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage">Azurite emulator</a> for local Azure Storage development, so I already had it installed. However, if you're running an earlier version of Visual Studio, you can install Azurite by using either Node Package Manager (npm), DockerHub, or by cloning the Azurite GitHub repository.</p>
<p>Another useful tool is the <a target="_blank" href="https://azure.microsoft.com/en-us/products/storage/storage-explorer/">Azure Storage Explorer</a>. This allows you to upload, download, and manage Azure Storage blobs, files, queues, and tables as well as configuring storage permissions and access controls, tiers, and rules.</p>
<p>One thing I did note was that to start the emulator, I needed to run the function app using F5. This started the Azurite background process, which then showed up in the storage explorer as follows:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/010-storage-viewer-emulated.png?raw=true" alt="Storage Explorer showing emulated storage" /></p>
<p>If I didn't do this, the storage explorer would tell me to install Azurite.</p>
<p>With the emulator up and running, the next step was to create some <a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction#containers">containers</a> for the payloads. I had decided to split the payloads into two containers, one for payloads that had passed validation (<code>webhook-payloads-accepted</code>) and were accepted for further processing and another for payloads that were rejected (<code>webhook-payloads-rejected</code>).</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/020-storage-viewer-emulated-containers.png?raw=true" alt="Storage Explorer showing containers in emulated storage" /></p>
<h2 id="heading-storing-the-payloads">Storing the payloads</h2>
<p>Before storing the payloads in the new containers, I needed to decide how they should be stored. Blob Storage is essentially a flat namespace, which means it doesn't have real directories or folders. However, it does support a folder-like structure using naming conventions and delimiters, typically the forward slash (<code>/</code>), within blob names.</p>
<p>Putting myself in the place of someone fielding a support query, I imagined being told that a tenant was expecting a payload from a particular third party on a specific day. With this hierarchy in mind, I decided upon the following 'folder' structure.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span> <span class="hljs-title">GetBlobName</span>(<span class="hljs-params">
    <span class="hljs-keyword">string</span> tenantId,
    <span class="hljs-keyword">string</span> senderId,
    <span class="hljs-keyword">string</span> messageId</span>)</span>
{
    <span class="hljs-keyword">var</span> blobName =
        <span class="hljs-string">$"<span class="hljs-subst">{tenantId}</span>/<span class="hljs-subst">{senderId}</span>/<span class="hljs-subst">{DateTime.UtcNow:yyyy-MM-dd}</span>/<span class="hljs-subst">{messageId}</span>.json"</span>;
    <span class="hljs-keyword">return</span> blobName;
}
</code></pre>
<p>The value of <code>messageId</code> is globally unique and will be passed back to the caller in a custom header. This adds another possible route for debugging calls between the systems.</p>
<p>Now we know how we are going to store the payloads, we need to use the SDK to store them. First of all we need a <code>BlobServiceClient</code> instance. It can be good practice to avoid over-instantiation of SDK clients. ChatGPT seemed to think that reusing the same instance is recommended in official Azure SDK documentation to improve performance and resource utilization. For production, I would double-check this, but for now that is good enough for me and so I stored the client at a module level.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> BlobServiceClient _blobServiceClient;

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">BlobPayloadStore</span>(<span class="hljs-params">ILoggerFactory loggerFactory</span>)</span>
{
    _logger = loggerFactory.CreateLogger&lt;BlobPayloadStore&gt;();
    _blobServiceClient = <span class="hljs-keyword">new</span> BlobServiceClient(<span class="hljs-string">"UseDevelopmentStorage=true"</span>);
}
</code></pre>
<p><code>BlobPayloadStore</code> was registered as a singleton with the dependency injection, so there was no need to have any statics involved. I.e., in <code>Program.cs</code>:</p>
<pre><code class="lang-csharp">.ConfigureServices(services =&gt;
{
    <span class="hljs-comment">// &lt;snip&gt;</span>
    services.AddSingleton&lt;IPayloadStore, BlobPayloadStore&gt;();
})
</code></pre>
<p>This just left me the task of writing the code to upload the payloads to the appropriate containers. The result is as follows.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">UploadPayloadAsync</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">
    <span class="hljs-keyword">string</span> containerName,
    <span class="hljs-keyword">string</span> tenantId,
    <span class="hljs-keyword">string</span> senderId,
    <span class="hljs-keyword">string</span> contractId,
    <span class="hljs-keyword">string</span> messageId,
    T payload</span>) <span class="hljs-keyword">where</span> T : PayloadBase</span>
{
    <span class="hljs-keyword">string</span> payloadJsonString = JsonConvert.SerializeObject(payload, Formatting.Indented);

    <span class="hljs-keyword">var</span> blobServiceClient = GetBlobServiceClient();
    <span class="hljs-keyword">var</span> containerClient = blobServiceClient.GetBlobContainerClient(containerName);

    <span class="hljs-keyword">var</span> blobName = GetBlobName(tenantId, senderId, contractId, messageId);
    <span class="hljs-keyword">var</span> blobClient = containerClient.GetBlobClient(blobName);

    <span class="hljs-keyword">var</span> byteArray = Encoding.UTF8.GetBytes(payloadJsonString);
    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> stream = <span class="hljs-keyword">new</span> MemoryStream(byteArray);

    <span class="hljs-keyword">await</span> blobClient.UploadAsync(stream, overwrite: <span class="hljs-literal">true</span>);
}
</code></pre>
<h2 id="heading-local-testing">Local testing</h2>
<p>After hitting F5 to run the function, I submitted a valid request to the local endpoint. Opening up Azure Storage Explorer, I could see a blob had been added as expected to the 'accepted' container.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/030-accepted-payload-local-storage.png?raw=true" alt="Local storage showing accepted payload" /></p>
<p>The Azure Storage Explorer has a handy feature to preview the contents. Using this, I inspected the contents and could see that they contained the expected details. I did note that the API key value was not present, so must have been very sensibly filtered out.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/040-accepted-payload-preview.png?raw=true" alt="Local storage showing accepted payload preview" /></p>
<p>I then ran a test with an invalid payload and, sure enough, a blob was added to the rejected container.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/050-rejected-payload-local-storage.png?raw=true" alt="Local storage showing rejected payload" /></p>
<p>Previewing this, I could see that the errors had been passed through as expected.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-04-blob-storage/060-rejected-payload-preview.png?raw=true" alt="Local storage showing rejected payload preview" /></p>
<h2 id="heading-cloud-considerations">Cloud considerations</h2>
<p>The next step is to deploy to the cloud and test there. However, this raises a number of questions.</p>
<ul>
<li>How should the Blob containers be exposed?<ul>
<li>Public vs. Private endpoints</li>
</ul>
</li>
<li>How should the Azure Function connect to the containers?<ul>
<li>Connection string?</li>
<li>Managed identity?</li>
</ul>
</li>
<li>If using a connection string, how should it be obtained?<ul>
<li>Environment variable?</li>
<li>Key vault?</li>
</ul>
</li>
</ul>
<p>Given these considerations, this feels like a post in itself. So I will leave it till next time.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, I showed how I was able to use the local Azure development tools to implement and test storing the request payloads. However, to get the functionality deployed and working in the cloud will require some more thought and experimentation.</p>
]]></content:encoded></item><item><title><![CDATA[Exposing an Azure Function using API Management]]></title><description><![CDATA[This is the third post in my series where I explore the world of serverless Azure. I am doing this by building a webhook proxy application using only the serverless Azure services. In the first two posts, I built and deployed an Azure function that r...]]></description><link>https://www.10printiamcool.com/exposing-an-azure-function-using-api-management</link><guid isPermaLink="true">https://www.10printiamcool.com/exposing-an-azure-function-using-api-management</guid><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Tue, 09 Apr 2024 10:59:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712576888997/79c3fa18-52b4-481e-85be-d8f4d0800fa8.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the third post in my <a target="_blank" href="https://www.10printiamcool.com/series/azure-serverless">series</a> where I explore the world of serverless Azure. I am doing this by building a webhook proxy application using only the serverless Azure services. In the first two posts, I built and deployed an Azure function that receives the webhooks and validates the request body against the corresponding schema. In this post, I look at using <a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts">Azure API Management (APIM)</a> in front of that function.</p>
<h2 id="heading-why-use-api-management">Why use API Management?</h2>
<p>As we saw in the <a target="_blank" href="https://www.10printiamcool.com/deploying-and-debugging-my-first-azure-function">previous post</a>, the Azure function is accessible from the public internet provided you know the appropriate API key. So why would you need a service such as API Management? A few reasons are listed below.</p>
<ul>
<li>Rate limiting, ensuring fair usage among consumers</li>
<li>Subscription-level control, such as key rotation</li>
<li>Advanced security, such as OAuth 2.0</li>
</ul>
<p>API Management has many more features that we won't explore in this post, but include the following:</p>
<ul>
<li>Centralized management of APIs</li>
<li>Customizable API facades</li>
<li>API Documentation and Developer Portals</li>
<li>API Analytics and Insights</li>
<li>Caching Mechanisms</li>
<li>Versioning and Revision Control</li>
</ul>
<p>I find so much in software design is down to positioning. By using a service such as API Management, we will position ourselves to provide production-level API.</p>
<h2 id="heading-are-there-any-alternatives">Are there any alternatives?</h2>
<p>As with other cloud providers, Azure offers services that somewhat overlap in what they offer. In this case, ChatGPT was able to offer the following options amongst some others:</p>
<h3 id="heading-azure-application-gateway-with-web-application-firewall-waf">Azure Application Gateway with Web Application Firewall (WAF)</h3>
<p><strong>Use Case</strong>: If you're primarily looking for API gateway capabilities with security features like a Web Application Firewall, SSL termination, and URL-based routing.</p>
<ul>
<li><strong>Why Use Over APIM</strong>: Offers Layer 7 load balancing with built-in WAF for security-focused scenarios, especially where protection against common web vulnerabilities and exploits is a priority.</li>
</ul>
<h3 id="heading-azure-functions-proxies">Azure Functions Proxies</h3>
<p><strong>Use Case</strong>: For lightweight API orchestration or when you need a simple facade in front of multiple Azure Functions.</p>
<ul>
<li><strong>Why Use Over APIM</strong>: It's a simple solution to create a single API surface for multiple microservices, particularly when these services are implemented using Azure Functions. However, it's less feature-rich compared to APIM.</li>
</ul>
<h3 id="heading-azure-front-door">Azure Front Door</h3>
<p><strong>Use Case</strong>: For global routing and load balancing needs, offering capabilities like URL-based routing, SSL termination, and global load balancing.</p>
<ul>
<li><strong>Why Use Over APIM</strong>: It is more focused on content delivery, global routing, and ensuring high availability and performance for your web applications and APIs.</li>
</ul>
<p>Given the API-focused nature of the application I am building, and the cost, API Management seems to be a good fit.</p>
<h2 id="heading-creating-the-api-management-instance">Creating the API Management instance</h2>
<p>Using the <a target="_blank" href="https://learn.microsoft.com/en-gb/azure/azure-functions/functions-openapi-definition">Expose serverless APIs from HTTP endpoints using Azure API Management</a> article as a guide, I opened up the Azure portal and navigated to my function app and selected the API Management blade.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/01-azure-portal-function-app-api-management.png?raw=true" alt="Function App API Management blade" /></p>
<p>This brought up the following option to create a new API Management instance.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/02-azure-portal-function-app-create-new-apim.png?raw=true" alt="Azure portal create API Management" /></p>
<p>Clicking on 'Create new' brought up the following UI. The Region and Resource name were defaulted and I filled in the other details. To save money, I chose the 'Developer' pricing tier noting that there is no Service Level Agreement (SLA) for this tier.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/03-azure-portal-function-create-apim-basics.png?raw=true" alt="Azure portal API Management basics" /></p>
<p>The next step gave the option to link the new instance with Application Insights and utilise 'Defender for Could' (sic). I enabled the former, but there wasn't an option to enable the latter. This may have been due to using the cheapest tier.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/04-azure-portal-function-create-apim-monitor-and-secure.png?raw=true" alt="Azure portal API Management monitor and secure" /></p>
<p>The final step was to choose the network connectivity. As this is to be a public endpoint, I chose 'None'.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/05-azure-portal-function-create-apim-virtual-network.png?raw=true" alt="Azure portal API Management network connectivity" /></p>
<p>After clicking 'Create' and waiting a while, the new API Management instance was ready to be connected to the function and expose the API.</p>
<h2 id="heading-exposing-the-azure-function-as-an-api">Exposing the Azure function as an API</h2>
<p>Now when I selected the API Management blade from the function app, it reported that it was now linked with the new instance. However, it had not imported anything automatically and presented me with the option to create a new API.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/06-apim-create-new-api.png?raw=true" alt="API Management UI to add an API" /></p>
<p>There was nothing promising in the API dropdown, so I selected 'Create New' and clicked on 'Link API'. I was then presented with the following list of Azure Functions.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/07-apim-select-azure-function.png?raw=true" alt="API Management UI listing Azure Functions" /></p>
<p>Now this was looking more promising. The wizard appears to have recognised my Azure Function. I selected it and tried to progress. However, rather confusingly, I was then prompted to 'Define a new API' by selecting from a list of potential sources. This wasn't what I was expecting, but I selected 'Function App' and clicked to progress.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/08-apim-define-a-new-api.png?raw=true" alt="API Management UI asking to Define a new API" /></p>
<p>When I was presented with the next screen, I felt that I had somewhat gone round in a circle. It looked very similar to one earlier when my Azure Function was listed. However, I decided to click 'Select' and continue.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/09-apim-import-azure-functions.png?raw=true" alt="API Management showing Import Azure Functions" /></p>
<p>I was then presented with a list of Function Apps from which to import functions. I selected my Function App and clicked to progress.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/10-apim-select-azure-function-app.png?raw=true" alt="API Management showing Select Azure Function App" /></p>
<p>The next step defaulted in a set of values, which all seemed reasonable to me. So I simply clicked 'Create'.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/11-apim-create-from-function-app.png?raw=true" alt="API Management showing Create from Function App" /></p>
<p>Once the creation was complete, I was able to view the 'Design' page for the new API and see the integration with the Azure Function in the backend. I could see how the process had recognised the request parameters and I also noticed the 'Inbound processing' box mentioned modifying the request. Also in the 'Inbound processing' box was the option to add policies. These, it turns out, are where you can do things such as filtering by IP address or rate limiting by key.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/12-apim-design-ui.png?raw=true" alt="API Management showing design of the API integration" /></p>
<p>My eye was also caught by the 'Test' tab, so I clicked on it and gave it a go.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/13-apim-test-ui-input.png?raw=true" alt="API Management showing the test UI" /></p>
<p>The response below showed that my Azure Function had been successfully called through the new API Management instance. So after quite a bit of clicking, it looks like I had managed to achieve my first aim.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/14-apim-test-ui-results.png?raw=true" alt="API Management showing the test UI" /></p>
<p>I was a bit curious as how API Management was authorised to access my Azure Function. A bit of searching found this in the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/import-function-app-as-api#authorization">Authorization</a> section of a Microsoft article:</p>
<blockquote>
<p>Import of an Azure Function App automatically generates:</p>
<ul>
<li>Host key inside the Function App with the name apim-{your Azure API Management service instance name},</li>
<li>Named value inside the Azure API Management instance with the name {your Azure Function App instance name}-key, which contains the created host key.</li>
</ul>
</blockquote>
<p>Sure enough, when I looked in the portal, I could see the following App key in my Function App.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/11a-function-app-apim-app-key.png?raw=true" alt="Function App app key" /></p>
<p>And in the API Management instance, I found the same value as a 'Named value' with the expected name.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/11b-apim-function-app-key-named-value.png?raw=true" alt="API Management Function App key named value" /></p>
<p>Ideally, I would like to used a Managed Identity and grant this identity access to the Azure Function. This method is more secure and manageable than function keys, but is only available to Premium Tier instances. So, for now, we will go with function keys.</p>
<h2 id="heading-creating-tenant-specific-subscriptions">Creating tenant-specific subscriptions</h2>
<p>The Azure portal test had shown that the function was successfully integrated with API Management. The next test was to invoke it from outside of the Azure portal. As it happens, the Azure portal test UI has a feature that makes this easy. There is an option to copy the test HTTP request, which is highlighted below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/15-apim-rest-client-input.png?raw=true" alt="API Management test UI option to copy the HTTP request" /></p>
<p>Using <a target="_blank" href="https://code.visualstudio.com/">VS Code</a> and the <a target="_blank" href="https://github.com/Huachao/vscode-restclient">REST Client</a> extension, you can paste the copied value into an <code>.http</code> file and you get the following.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/15-apim-rest-client-data.png?raw=true" alt="VS Code showing pasted HTTP request" /></p>
<p>So all I needed to do was find a suitable subscription key. I went into the API Management UI and the Subscriptions blade. This listed three subscription keys that appeared to have been created by default.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/16-apim-subscription-list.png?raw=true" alt="API Management UI showing subscription list" /></p>
<p>I tried the first two primary keys, but neither worked. So, with some reluctance, I tried to select the 'all-access' key. The result was this warning.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/17-apim-subscription-warning.png?raw=true" alt="API Management UI showing warning about using the all-access subscription" /></p>
<p>I decided to persevere and was able to successfully use the all-access key to make calls from VS Code. However, this was far from ideal and I wanted to work out how I would be able to issue and maintain separate keys for separate callers. So I clicked the option to create a new subscription.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/17a-apim-create-new-subscription.png?raw=true" alt="API Management UI showing create new subscription wizard" /></p>
<p>This was more like it. I could restrict the access of the subscription to a specific API. There was also an option to trace, but for now I did not explore it. Once created, the new subscription appeared in the list as shown below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/17b-apim-new-subscription-list.png?raw=true" alt="API Management UI showing new subscription in list" /></p>
<p>I copied the primary key locally and re-ran my test from VS Code. As hoped, this was successful and now I had a way of issuing and rotating keys that were scoped to just a specific API.</p>
<p>There was one more thing that was bugging me though. It was still possible to call the Azure Function directly from the public internet. Although this would require the function key to be leaked, it was far from ideal. So I started to look into how I could restrict access and remove this possibility.</p>
<h2 id="heading-disabling-public-access-to-the-function">Disabling public access to the function</h2>
<p>I went into the Networking blade for the Function App and clicked on the link next to 'Public network access'. This brought up the options below with the 'Enabled from all networks' selected. My first thought was to disable all public access. Maybe access from API Management doesn't count as public.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/18-function-app-disable-public-access.png?raw=true" alt="Function App UI showing all public network access disabled" /></p>
<p>However, a quick test showed that this wasn't the case. Sure, direct access had been stopped, but so had access through API Management.</p>
<p>Engaging with ChatGPT, there was mention of virtual networks and managed identities. The former I didn't want to get into at that point in time and the latter wasn't available at my pricing tier.</p>
<p>I asked ChatGPT "Is it possible to add an access rule to an Azure Function that restricts access only from Azure?" The response came back:</p>
<blockquote>
<p>Yes, it is possible to restrict access to an Azure Function so that it's accessible only from within Azure services. This can be done by configuring network access restrictions in your Azure Function settings. However, it's important to note that "from Azure" can encompass a wide range of sources, including other Azure services, VMs hosted in Azure, or services running in Azure regions.</p>
</blockquote>
<p>The suggestion from ChatGPT was to use an Azure Service tag in the network access restrictions. As it put it:</p>
<blockquote>
<p>Azure Service Tags represent a group of IP address prefixes from a given Azure service, which are used to help minimize complexity for security rule creation. You can leverage these in your network security rules.</p>
</blockquote>
<p>Given this, I selected the option to enable from selected IP address.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/19-function-app-enable-selected-access.png?raw=true" alt="Function App UI showing public network access enabled from selected endpoints" /></p>
<p>This brought up a list of rules and I clicked on the option to add a new one. Following ChatGPT's advice, I added a rule for the <code>AzureCloud</code> service tag.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/20-function-app-add-service-tag-rule.png?raw=true" alt="Function App UI to add an access rule" /></p>
<p>Once added, my new rule took pride of place at the top of the list. The portal defaults the unmatched rule action to 'Allow'. This isn't what I wanted, so I changed it to 'Deny' which resulted in the rule at the bottom.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-03-adding-api-management/21-function-app-rule-list.png?raw=true" alt="Function App UI rule list showing new rule" /></p>
<p>Again I tested. This time I was able to access the function through API Management, but not directly. So I now had the behaviour that I wanted. However, ChatGPT did highlight these valid considerations for using <code>AzureCloud</code> Service Tag:</p>
<blockquote>
<ul>
<li><p><strong>Broad Access</strong>: The <code>AzureCloud</code> service tag is quite broad and includes all of Azure's public IP addresses. It doesn't restrict access to only your Azure services but allows access from any Azure-hosted service, which can include Azure services used by others.</p>
</li>
<li><p><strong>Other Azure Services</strong>: If your intention is to allow access only from specific Azure services (like Azure Logic Apps, Azure VMs, etc.), you might need a more granular approach. You can specify the IP addresses or ranges of those specific services or use relevant service tags if available.</p>
</li>
</ul>
</blockquote>
<h2 id="heading-summary">Summary</h2>
<p>API Management appears to be a powerful tool to expose and manage external APIs. I barely scratched the surface of its capabilities, as I was satisfied in just knowing that my Azure Function was now behind a suitable service. There was an awful lot of clicking and it makes me wonder about how all this would be done through infrastructure as code.</p>
<p>Comparing this experience to the one I have had with <a target="_blank" href="https://aws.amazon.com/">AWS</a> is interesting. With Azure, I had to find ways to stop my Azure Function from being exposed. With AWS, you have to find ways to expose your Lambda functions. With Azure, you have to pay for managed identity functionality to integrate API Management with Azure Functions. With AWS, you have to use <a target="_blank" href="https://aws.amazon.com/iam/">Identity and Access Management (IAM)</a> for everything and it is completely free to use.</p>
<p>However, I now have the front of my application in a place I want. So the next step is to look at extending the back-end functionality, which will mean integrating with <a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction">Blob storage</a>.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts">What is Azure API Management?</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-gb/azure/azure-functions/functions-openapi-definition">Expose serverless APIs from HTTP endpoints using Azure API Management</a></p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-gb/azure/azure-functions/openapi-apim-integrate-visual-studio">Create serverless APIs in Visual Studio using Azure Functions and API Management integration</a></li>
</ul>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/import-function-app-as-api">Import an Azure Function App as an API in Azure API Management</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/get-started-create-service-instance">Quickstart: Create a new Azure API Management instance by using the Azure portal</a></p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/quickstart-bicep?tabs=CLI">Quickstart: Create a new Azure API Management service instance using Bicep</a></li>
</ul>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/observability">Observability in Azure API Management</a></p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-app-insights?tabs=rest">How to integrate Azure API Management with Azure Application Insights</a></li>
</ul>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/authentication-authorization-overview">Authentication and authorization to APIs in Azure API Management</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-gb/azure/api-management/protect-with-defender-for-apis?WT.mc_id=Portal-Microsoft_Azure_ApiManagement">Enable advanced API security features using Microsoft Defender for Cloud</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Deploying and Debugging my First Azure Function]]></title><description><![CDATA[In this series, I am learning by doing and documenting my experience on the way. I am aiming to build a serverless webhook proxy, to provide a resilient interface for downstream systems. In the first part, I used the local development tools to build ...]]></description><link>https://www.10printiamcool.com/deploying-and-debugging-my-first-azure-function</link><guid isPermaLink="true">https://www.10printiamcool.com/deploying-and-debugging-my-first-azure-function</guid><category><![CDATA[serverless]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Mon, 01 Apr 2024 08:49:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711961208787/705c54c7-34d5-49e5-b582-a6dfc7f583ad.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this <a target="_blank" href="https://www.10printiamcool.com/series/azure-serverless">series</a>, I am learning by doing and documenting my experience on the way. I am aiming to build a serverless webhook proxy, to provide a resilient interface for downstream systems. In the <a target="_blank" href="https://www.10printiamcool.com/observations-on-developing-my-first-azure-function">first part</a>, I used the local development tools to build a basic function to validate and store inbound requests. In this part, I deploy and debug the function in cloud, using the tools in Visual Studio and the Azure portal.</p>
<h2 id="heading-its-all-gone-south">It's all gone south</h2>
<p>Ultimately, I want to deploy the final application using infrastructure as code. However, first I thought I would try the ClickOps approach. This is done by right-clicking on the project and selecting 'Publish'.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/publish-from-vs.png?raw=true" alt="Visual Studio option to publish an Azure Function" /></p>
<p>The first time through it did successfully publish the function to Azure. However, I did notice something odd in the wizard. At one point it asks you to create a new Functions instance. The odd thing is that it only gave me one option for the storage, and that option was on the other side of the world.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-australia-storage.png?raw=true" alt="Creating a function instance with storage in Australia Central" /></p>
<p>I thought that perhaps it was perhaps a user interface issue. Surely, it wouldn't create resources 12,000 miles apart. I clicked to continue and then looked at the created resources.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/resources-australia-central.png?raw=true" alt="Resource group view showing storage in Australia Central" /></p>
<p>Unfortunately, it was very much the case that resources could not have been more geographically disparate if you tried.</p>
<h2 id="heading-back-to-the-portal">Back to the Portal</h2>
<p>Try as I might, I could not get the Visual Studio wizard to create a set of geographically-sensible resources. Instead, I went into the Azure portal and selected the option to create a Function App. This did allow me full control over the location of any created resources. In particular, the Application Insights could now be located in the UK.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/app-insights-closer-to-home.png?raw=true" alt="The monitoring page of the portal Function App wizard" /></p>
<p>Once the Function App had been created via the portal, I could select it in the Visual Studio wizard.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/portal-created-function-app.png?raw=true" alt="Visual Studio publish wizard showing manually-created resources" /></p>
<p>Once this wizard had completed, and generated a few interesting files, I was presented by the 'Publish' button below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/azure-publish-profile.png?raw=true" alt="Publish option within Visual Studio" /></p>
<p>Pressing this started the deployment and, before long, I had my function deployed into the cloud and ready to be tested.</p>
<h2 id="heading-debugging-the-deployment">Debugging the deployment</h2>
<p>Full of excitement, I fired off a request to the function and got the following response.</p>
<pre><code class="lang-text">HTTP/1.1 500 Internal Server Error
</code></pre>
<p>Now, I could test the function locally to recreate and debug the issue. However, it struck me that it would be an opportunity to look at what diagnostics are available in the cloud. With this in mind, I opened the Azure portal and went into the function app overview. Here, I was presented with a list of functions.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/app-overview-function-list.png?raw=true" alt="Azure portal Function list shown in Function App overview" /></p>
<p>After selecting the function, I was then given the following developer options.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-developer-options.png?raw=true" alt="Azure portal Function developer options" /></p>
<p>Out of curiosity, I selected the 'Integration' option and got a diagram showing how the triggers and inputs for the function and the outputs from the function. There was also a warning me that I could not edit my function in the portal, as I had chosen to use the isolated worker model. If such editing is important to you, then this is perhaps a reason not to choose that model. For me, I would rather not have the option.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-integration-overview.png?raw=true" alt="Azure portal Function App integration overview" /></p>
<p>Selecting 'Monitor' resulted in a promising list of function invocations. At the top of the list was my failure.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-invocation-list.png?raw=true" alt="Azure portal Function App invocation list" /></p>
<p>Clicking on the hyperlinked date brought up the details that I was looking for.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-invocation-details.png?raw=true" alt="Azure portal Function App invocation details" /></p>
<p>Here was the exception stack trace that clearly showed that the error lay in the code I had written. Well, the code I had copied from ChatGPT. Clearly, I had not been as diligent as I should have been with my testing.</p>
<p>There was also an option to run a query in Application Insights. Clicking this caused the following query to run and return all the relevant entries. Note how the query uses the 'union' operator to combine data from both the <code>traces</code> and <code>exceptions</code> tables.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-invocation-app-insights.png?raw=true" alt="Azure portal Function App invocation shown in App Insights" /></p>
<p>For completeness, I open the 'Logs' page. This appeared to be some sort of realtime view of the logs. I ran my faulty function again and saw the following entries appear.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/function-app-monitor-logs.png?raw=true" alt="Azure portal Function App Monitor Logs" /></p>
<p>By default, it doesn't appear to log the actual exception. This seems to rather limit its usage, as the other views capture the full details. However, it might have some uses that are not apparent to me at the moment. It is good to know it is there though.</p>
<p>Now I knew what the problem was, I could go and fix it. But before that, there was one more thing I wanted to try and that was remote debugging.</p>
<h2 id="heading-remote-debugging-eventually">Remote debugging (eventually)</h2>
<p>To tell the truth, I could have saved myself quite a bit of frustration if I had read the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-vs?pivots=isolated#remote-debugging">remote debugging</a> section of the Microsoft <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-vs?pivots=isolated">Develop Azure Functions using Visual Studio</a> guide. However, here is the tale of my more circuitous route to success.</p>
<p>My first attempt was the most obvious option. That is, to use the option in the Publish page to attach a debugger.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-attach-from-publish.png?raw=true" alt="Visual Studio option in Publish page to attach debugger" /></p>
<p>This then indicated some activity, but ultimately no attachment occurred. Undeterred, I searched the internet and found the blog post <a target="_blank" href="https://turbo360.com/blog/remote-debugging-azure-functions-in-visual-studio-2022">How to remote debug an HTTP trigger Azure Functions in Visual Studio 2022</a>. Amongst the steps mentioned, was to enable remote debugging in the Azure portal. However, when I looked I found this was already enabled.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-enable-on-portal.png?raw=true" alt="Function App configuration in Azure portal to enable remote debugging" /></p>
<p>With hindsight, what I suspect had happened was that the 'Attach Debugger' operation had enabled this. The <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-vs?pivots=isolated#disable-remote-debugging">Microsoft article</a> advises the following:</p>
<blockquote>
<p>After you're done remote debugging your code, you should disable remote debugging in the Azure portal. Remote debugging is automatically disabled after 48 hours, in case you forget.</p>
</blockquote>
<p>The next thing I tried was to update the publish settings. The configuration was set to 'Release', so I changed it to 'Debug'.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-publish-settings.png?raw=true" alt="Visual Studio publish settings" /></p>
<p>Following the instructions in the blog post, I tried manually attaching to the remote process and was prompted for credentials to connect.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-credentials-prompt.png?raw=true" alt="Azure portal prompting for credentials to attach remote debugger" /></p>
<p>The credentials required had to be downloaded from the Azure portal, via a publish profile.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-download-publish-profile.png?raw=true" alt="Option to download publish profile from Azure portal" /></p>
<p>In the downloaded file, I found the details required in the 'Zip Deploy' element.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-downloaded-publish-profile.png?raw=true" alt="Downloaded publish profile for zip deploy" /></p>
<p>After a few tries, I was finally able to see the processes. So I followed the blog post and tried attaching to the <code>w3wp.exe</code> process.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-remote-w3wp.png?raw=true" alt="Selecting the remote w3wp process to attach the debugger" /></p>
<p>However, Visual Studio still reported that my breakpoints were not active.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-no-symbols-loaded.png?raw=true" alt="Visual Studio showing no symbols loaded" /></p>
<p>I wondered for a short while if remote debugging was not supported for the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows">isolated worker model</a>. Then it dawned on me that it wasn't the <code>w3wp.exe</code> process that I should be attaching to, it was the isolated <code>dotnet.exe</code> process instead.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-remote-dotnet-process.png?raw=true" alt="Selecting the remote dotnet process to attach the debugger" /></p>
<p>Once I had done this, everything fell into place. My breakpoint was hit and I could step through my function remotely.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-02-deploying-my-first-function/remote-debugging-breakpoint-hit.png?raw=true" alt="Visual Studio showing breakpoint being hit" /></p>
<p>As mentioned, I could have avoided this, as the Microsoft article clearly states:</p>
<blockquote>
<p>Check <strong>Show process from all users</strong> and then choose <strong>dotnet.exe</strong> and select <strong>Attach</strong>.</p>
</blockquote>
<h2 id="heading-summary">Summary</h2>
<p>My experience highlighted the upsides and downsides of high-level wizards. When they work, they can be very productive. However, when they don't, it can be very difficult to understand what is going on an how to fix it. I also fell foul of the changing technology, as I was reading articles for the older function model whilst using the newer model.</p>
<p>However, I got there in the end and the option of remote debugging is an interesting one. It is one that I have never really felt I needed with AWS Lambda functions, but perhaps I will find a use now it is available. The option does expose the internals of the isolated worker model, as you can see how there is a separate process being called from the usual <code>w3wp.exe</code> process. If you step through an unhandled exception, you can even see how <a target="_blank" href="https://grpc.io/">gRPC</a> is used to communicate. All quite different from the black box of AWS Lambda functions.</p>
]]></content:encoded></item><item><title><![CDATA[Observations on Developing My First Azure Function]]></title><description><![CDATA[Recently, my focus at work has shifted from AWS development to Azure development. To help myself get properly acquainted with the technology, I have decided to set myself a Azure-based serverless challenge. That is, to build a multi-tenant webhook pr...]]></description><link>https://www.10printiamcool.com/observations-on-developing-my-first-azure-function</link><guid isPermaLink="true">https://www.10printiamcool.com/observations-on-developing-my-first-azure-function</guid><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 24 Mar 2024 11:08:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711278380078/405368f6-ec30-424d-9aca-3e00dd238826.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, my focus at work has shifted from <a target="_blank" href="https://aws.amazon.com/">AWS</a> development to <a target="_blank" href="https://portal.azure.com/">Azure</a> development. To help myself get properly acquainted with the technology, I have decided to set myself a Azure-based serverless challenge. That is, to build a multi-tenant webhook proxy. First using <a target="_blank" href="https://docs.cloudposse.com/glossary/clickops/">ClickOps</a> and then using <a target="_blank" href="https://learn.microsoft.com/en-us/devops/deliver/what-is-infrastructure-as-code">Infrastructure as Code (IaC)</a>. In this post, I will first look at writing a single <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview">Azure Function</a> and see what that brings.</p>
<h2 id="heading-the-webhook-proxy-application">The Webhook Proxy Application</h2>
<p>The ultimate goal is to create a serverless application that can be placed in front of internal systems to robustly handle webhook callbacks. Its functionality will cover:</p>
<ul>
<li>Validating the content of the callback</li>
<li>Storing the content of the callback</li>
<li>Forwarding the content to a downstream system</li>
<li>Automatically retrying if the downstream system is offline</li>
<li>Forwarding to a dead letter queue if not able to forward</li>
</ul>
<p>In AWS, I would probably build the application using API Gateway calling a Lambda function, that stored the request in S3. Then handle the S3 events raised, using a combination of Lambda functions and Event Bridge to deliver the request.</p>
<p>The appealing aspect of this task is that it is a real-world need, and that it covers API management, functions as a service, serverless storage, and events. This means I will need to tangle with the following Azure technologies:</p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview">Functions</a></li>
<li><a target="_blank" href="https://azure.microsoft.com/en-us/products/storage/blobs/">Blob Storage</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts">API Management</a></li>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview">Service Bus</a></li>
</ul>
<p>My first step on this journey is to create, test, and deploy an Azure Function that validates the contents of an HTTP according to a schema specified as part of the path.</p>
<h2 id="heading-choosing-an-azure-function-model">Choosing an Azure Function model</h2>
<p>Things rarely turn out to be as straightforward as you think, and straight away I was forced to make a choice as to the Azure Function model to use. Since I last explored them, there is a new 'isolated worker model' for running Azure Functions. The Microsoft article on the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-in-process-differences">differences between the isolated worker model and the in-process model</a> explains that there are two execution models for .NET functions:</p>
<blockquote>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Execution model</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>Isolated worker model</td><td>Your function code runs in a separate .NET worker process. Use with supported versions of .NET and .NET Framework.</td></tr>
<tr>
<td>In-process model</td><td>Your function code runs in the same process as the Functions host process. Supports only Long Term Support (LTS) versions of .NET.</td></tr>
</tbody>
</table>
</div></blockquote>
<p>The <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows">Guide for running C# Azure Functions in an isolated worker process</a> explains the benefits for the newer model:</p>
<blockquote>
<ul>
<li>Fewer conflicts: Because your functions run in a separate process, assemblies used in your app don't conflict with different versions of the same assemblies used by the host process.</li>
<li>Full control of the process: You control the start-up of the app, which means that you can manage the configurations used and the middleware started.</li>
<li>Standard dependency injection: Because you have full control of the process, you can use current .NET behaviors for dependency injection and incorporating middleware into your function app.</li>
</ul>
</blockquote>
<p>In my experience, when Microsoft develop a new model then it is better to adopt it if you can. The older models do not seem to get quite the same love. So given that, it was the isolated worker model for me.</p>
<h2 id="heading-the-out-of-the-box-experience">The 'Out of the Box' Experience</h2>
<p>The Microsoft article <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-create-your-first-function-visual-studio">Create your first C# function in Azure using Visual Studio</a> was my guide to getting started. The experience compared to Lambda functions in AWS is quite stark. Visual Studio is a one-stop development environment for .NET, whereas AWS and VS Code feels like an 'assemble your own' adventure.</p>
<p>As long as you remember to select the Azure development workload during installation of Visual Studio, then it only took a few steps after selecting the 'Azure Functions` template before I could hit F5 and have the following code executing:</p>
<pre><code class="lang-c#">[<span class="hljs-meta">Function(<span class="hljs-meta-string">"HttpExample"</span>)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> IActionResult <span class="hljs-title">Run</span>(<span class="hljs-params">
  [HttpTrigger(AuthorizationLevel.Anonymous, <span class="hljs-string">"get"</span>, <span class="hljs-string">"post"</span></span>)] HttpRequest req)</span>
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> OkObjectResult(<span class="hljs-string">"Welcome to Azure Functions!"</span>);
}
</code></pre>
<p>Visual Studio had seamlessly started a local hosting process at <a target="_blank" href="http://localhost:7166/api/HttpExample">http://localhost:7166/api/HttpExample</a>, as can be seen below:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/serverless-azure-01-developing-my-first-function/local-hosting.png?raw=true" alt="Command window showing the local hosting of an Azure Function" /></p>
<p>So straight away, I could set break points and benefit from a super-quick inner development loop. However, it turned out that the generated code was not the only way to go.</p>
<h3 id="heading-the-built-in-http-model">The Built-in HTTP model</h3>
<p>As the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows#http-trigger">HTTP trigger</a> documentation states:</p>
<blockquote>
<p>HTTP triggers allow a function to be invoked by an HTTP request. There are two different approaches that can be used:</p>
<ul>
<li>An ASP.NET Core integration model that uses concepts familiar to ASP.NET Core developers</li>
<li>A built-in model, which doesn't require extra dependencies and uses custom types for HTTP requests and responses.</li>
</ul>
</blockquote>
<p>I consulted with ChatGPT, which had the opinion:</p>
<blockquote>
<p>The choice between the built-in Azure Functions HTTP trigger model and ASP.NET Core integration depends on the complexity of your application, the need for control over the HTTP pipeline, and your familiarity with ASP.NET Core. For simpler, serverless applications, the built-in model is often the best choice due to its simplicity and tight integration with Azure. For more complex applications requiring the full feature set of ASP.NET Core, integrating with ASP.NET Core would be more appropriate.</p>
</blockquote>
<p>Probing ChatGPT a bit more as to the limitations of the Built-in HTTP model, I got the following scenario where the more complex model might be needed:</p>
<blockquote>
<p>While the built-in model of Azure Functions is powerful for many use cases, especially those that fit well within a serverless paradigm, it has its limitations in scenarios requiring advanced control over HTTP pipeline processing, complex authentication and authorization, and other sophisticated web API features. In these cases, integrating with a more feature-rich framework like ASP.NET Core would be more appropriate.</p>
</blockquote>
<p>Given that I intend to front the function with <a target="_blank" href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts">Azure API Management</a> to handle the authorisation aspects, I decided to go with the simplicity of the built-in model. This meant that the original boilerplate code became the following:</p>
<pre><code class="lang-c#">[<span class="hljs-meta">Function(<span class="hljs-meta-string">"HttpExample"</span>)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> HttpResponseData <span class="hljs-title">Run</span>(<span class="hljs-params">
  [HttpTrigger(AuthorizationLevel.Anonymous, <span class="hljs-string">"get"</span></span>)] HttpRequestData req)</span>
{
    <span class="hljs-keyword">var</span> response = req.CreateResponse(HttpStatusCode.OK);
    response.WriteString(<span class="hljs-string">"Welcome to Azure Functions!"</span>);
    <span class="hljs-keyword">return</span> response;
}
</code></pre>
<p>So a little more verbose, but I could get rid of a dependency which was nice. I did have to amend <code>Program.cs</code> to use the defaults for a worker process:</p>
<pre><code class="lang-c#"><span class="hljs-keyword">var</span> host = <span class="hljs-keyword">new</span> HostBuilder()
    <span class="hljs-comment">// Was ConfigureFunctionsWebApplication()</span>
    .ConfigureFunctionsWorkerDefaults()
    .Build();
</code></pre>
<h3 id="heading-dependency-injection-and-logging">Dependency Injection and Logging</h3>
<p>One significant difference that struck me between AWS Lambda functions and Azure Functions, was that the latter starts you off down the dependency injection (DI) route. The boilerplate code uses DI out of the box to inject an <code>ILoggerFactory</code> implementation that can then be used to obtain a logger:</p>
<pre><code class="lang-c#"><span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">SimpleHttpFunction</span>(<span class="hljs-params">ILoggerFactory loggerFactory</span>)</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ILogger _logger =
      loggerFactory.CreateLogger&lt;SimpleHttpFunction&gt;();

    [<span class="hljs-meta">Function(<span class="hljs-meta-string">"HttpExample"</span>)</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> HttpResponseData <span class="hljs-title">Run</span>(<span class="hljs-params">
      [HttpTrigger(AuthorizationLevel.Anonymous, <span class="hljs-string">"get"</span></span>)] HttpRequestData req)</span>
    {
        _logger.LogInformation(<span class="hljs-string">"C# HTTP trigger function processed a request."</span>);
</code></pre>
<p>As with a 'normal' application, the logging level is controlled within the <code>Program.cs</code> file. Here I want to have a very verbose output in development, but leave the level when in higher environments.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">var</span> host = <span class="hljs-keyword">new</span> HostBuilder()
    <span class="hljs-comment">// ...</span>
    .ConfigureLogging((context, loggingConfig) =&gt;
    {
        <span class="hljs-keyword">var</span> env = context.HostingEnvironment;
        <span class="hljs-keyword">if</span> (env.IsDevelopment())
            loggingConfig.SetMinimumLevel(LogLevel.Trace);
    })
    <span class="hljs-comment">// ...</span>
    .Build();
</code></pre>
<p>I decided to take advantage of the dependency injection and adopt a little <a target="_blank" href="&lt;https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)&gt;">hexagonal architecture</a>. I created a couple of services to isolate my function from the details of how a request is validated and how the request content is stored.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">var</span> host = <span class="hljs-keyword">new</span> HostBuilder()
    <span class="hljs-comment">// ...</span>
    .ConfigureServices(services =&gt;
    {
        services.AddSingleton&lt;IRequestValidator, RequestValidator&gt;();
        services.AddSingleton&lt;IRequestStore, BlobRequestStore&gt;();
    })
    .Build();
</code></pre>
<p>With this in place, I could then use a <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors">primary constructor</a> to have the implementations injected at runtime. This would also set me nicely to do some unit testing later on.</p>
<pre><code class="lang-c#"><span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">ValidateAndStoreFunction</span>(<span class="hljs-params">
    ILoggerFactory loggerFactory,
    IRequestValidator requestValidator,
    IRequestStore requestStore</span>)</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ILogger _logger =
        loggerFactory.CreateLogger&lt;ValidateAndStoreFunction&gt;();
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IRequestValidator _requestValidator = requestValidator;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IRequestStore _requestStore = requestStore;
</code></pre>
<p>As mentioned earlier, this all feels quite different to working with AWS Lambda functions. In AWS, it felt like the onus was on keeping everything as light as possible. I have never used any dependency injection frameworks with AWS, so this felt much more like 'normal' application development. My concern, as with all serverless functions, was that this would add to any cold start times. However, for my learning application, I am not too concerned. For those that are, Mikhail Shilkov has written this excellent article on <a target="_blank" href="https://mikhail.io/serverless/coldstarts/azure/">Cold Starts in Azure Functions</a>.</p>
<h3 id="heading-unit-testing-my-function">Unit testing my function</h3>
<p>Whilst it was great to be able to run my function and invoke it from <a target="_blank" href="https://curl.se/docs/manpage.html">cURL</a> or <a target="_blank" href="https://www.postman.com/">Postman</a>, I like to write pure unit tests that can be run from anywhere. This turned out to be quite straightforward, at least in the case of my function.</p>
<p>The first task was to instantiate and instance of <code>ValidateAndStoreFunction</code>. The primary constructor for this is as follows:</p>
<pre><code class="lang-c#">ValidateAndStoreFunction(
    ILoggerFactory loggerFactory,
    IRequestValidator requestValidator,
    IRequestStore requestStore)
</code></pre>
<p>Using the <a target="_blank" href="https://github.com/devlooped/moq">Moq</a> mocking framework, I was able to supply mocks for these and setup appropriate return values.</p>
<pre><code class="lang-c#">_mockLoggerFactory = <span class="hljs-keyword">new</span> Mock&lt;ILoggerFactory&gt;();
_mockRequestValidator = <span class="hljs-keyword">new</span> Mock&lt;IRequestValidator&gt;();
_mockRequestStore = <span class="hljs-keyword">new</span> Mock&lt;IRequestStore&gt;();
</code></pre>
<p>The first real snag was the signature of the <code>Run</code> method. It requires a <code>HttpRequestData</code> as input:</p>
<pre><code class="lang-c#"><span class="hljs-function">HttpResponseData <span class="hljs-title">Run</span>(<span class="hljs-params">
    HttpRequestData req,
    <span class="hljs-keyword">string</span> contractId,
    <span class="hljs-keyword">string</span> senderId,
    <span class="hljs-keyword">string</span> tenantId</span>)</span>
</code></pre>
<p>It turns out that <code>HttpRequestData</code> is an abstract class, so I tried to subclass it. This hit a couple of issues. The first was that <code>HttpRequestData</code> has a constructor that requires a <code>FunctionContext</code> instance. <code>FunctionContext</code> is itself abstract, so I tried using Moq to provide one.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">class</span> <span class="hljs-title">MockHttpRequestData</span>()
    : <span class="hljs-title">HttpRequestData</span>(<span class="hljs-title">new</span> <span class="hljs-title">Mock</span>&lt;<span class="hljs-title">FunctionContext</span>&gt;().<span class="hljs-title">Object</span>)
</code></pre>
<p><code>HttpRequestData</code> also need to be able to create an <code>HttpResponseData</code> instance. The solution, again I subclassed <code>HttpResponseData</code> and return an instance of my new class.</p>
<pre><code class="lang-c#"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> HttpResponseData <span class="hljs-title">CreateResponse</span>(<span class="hljs-params"></span>)</span> =&gt; <span class="hljs-keyword">new</span> MockHttpResponseData();
</code></pre>
<p>The final step was to be able to pass in an object to be returned as a JSON stream.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">class</span> <span class="hljs-title">MockHttpRequestData</span>(<span class="hljs-title">object</span> <span class="hljs-title">bodyObject</span>)
    : <span class="hljs-title">HttpRequestData</span>(<span class="hljs-title">new</span> <span class="hljs-title">Mock</span>&lt;<span class="hljs-title">FunctionContext</span>&gt;().<span class="hljs-title">Object</span>)
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _bodyJson = JsonConvert.SerializeObject(bodyObject);

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> Stream Body =&gt; GetStringAsStream(_bodyJson);
</code></pre>
<p>Now with my mocks in place, I could create some nice simple unit tests that would run anywhere.</p>
<pre><code class="lang-c#"><span class="hljs-comment">// Arrange</span>
<span class="hljs-keyword">var</span> validateAndStoreSUT = <span class="hljs-keyword">new</span> ValidateAndStoreFunction(
    _mockLoggerFactory.Object, _mockRequestValidator.Object, mockRequestStore.Object);

<span class="hljs-comment">// Act</span>
<span class="hljs-keyword">var</span> response =
    validateAndStoreSUT.Run(
        <span class="hljs-keyword">new</span> MockHttpRequestData(
            <span class="hljs-keyword">new</span> { }), ExpectedContractId, ExpectedSenderId, ExpectedTenantId);

<span class="hljs-comment">// Assert</span>
response.Should().NotBeNull();
response.StatusCode.Should().Be(HttpStatusCode.Created);
</code></pre>
<p>I don't know if other trigger would be more difficult to mock out, but the ease of mocking <code>HttpRequestData</code> gives me cause for optimism.</p>
<h2 id="heading-deploying-to-azure">Deploying to Azure</h2>
<p>My original intention was to finish this post with deploying to Azure from Visual Studio. However, this did not prove as straightforward as thought and I will defer the trials and tribulations to the next post.</p>
<h2 id="heading-summary">Summary</h2>
<p>Here are my observations on my first real experience with Azure Functions:</p>
<ul>
<li><p>Azure Functions feel more like a traditional application than AWS Lambda functions. With dependency injection and a local development experience.</p>
</li>
<li><p>The two available models, in-process and isolated, complicates initial decisions.</p>
</li>
<li><p>Strong support for the local (F5) experience. This makes it easy to get going and there is support for remote debugging too that I didn't have time to investigate.</p>
</li>
<li><p>The middleware limitations stopped me in my tracks. I was hoping to be able to extend my functions with middleware that would short-circuit the pipeline, but this turned out not to be possible.</p>
</li>
<li><p>Unit testing was straightforward, with support for dependency injection and the framework classes making it easy to mock the inputs to the function.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My First AI-Assisted Project]]></title><description><![CDATA[In this post, I share my experience of learning how to use ChatGPT to assist me develop a tool to assist me in solving a real world problem. A problem in a domain where I have limited knowledge.
Every February, under the guise of Andy Balham, I take ...]]></description><link>https://www.10printiamcool.com/my-first-ai-assisted-project</link><guid isPermaLink="true">https://www.10printiamcool.com/my-first-ai-assisted-project</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Mon, 08 Jan 2024 19:12:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704740944048/dbc3a824-83ff-4d6f-a015-4f358412767e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I share my experience of learning how to use ChatGPT to assist me develop a tool to assist me in solving a real world problem. A problem in a domain where I have limited knowledge.</p>
<p>Every February, under the guise of Andy Balham, I take part in <a target="_blank" href="https://fawm.org/">FAWM (February is Album Writing Month)</a>. This is an online challenge to write and record 14 songs in 28 days (14.5 in a leap year!). The problem I had was that the resulting MP3 files need to be hosted, but my usual option was is not available this year.</p>
<p>So I set myself the challenge of finding a hosting alternative and, to provide a bit more of a challenge, develop a simple HTML interface on which to view and play the resulting songs.</p>
<h2 id="heading-what-are-my-options">What are my options?</h2>
<p>I first started asking ChatGPT about hosting static websites in <a target="_blank" href="https://aws.amazon.com/">AWS</a>, as I have experience in that area. However, when I asked ChatGPT the following question I got an interesting suggestion:</p>
<blockquote>
<p>How can I create a simple static website that looks good?</p>
</blockquote>
<p>The part that caught my eye was 'GitHub Pages' (emphasis mine):</p>
<blockquote>
<p><strong>Deploy Your Website:</strong></p>
<p>Once you're satisfied with the look and feel:</p>
<ul>
<li>Host it on platforms like <strong>GitHub Pages</strong>, Netlify, Vercel, or any web hosting provider.</li>
</ul>
</blockquote>
<p>I hadn't heard of this, so I prompted ChatGPT for more information:</p>
<blockquote>
<p>Can you tell me more about GitHub pages please?</p>
</blockquote>
<p>ChatGPT duly responded:</p>
<blockquote>
<p>GitHub Pages is a free web hosting service offered by GitHub that allows you to easily publish static websites directly from your GitHub repositories. It's a convenient way to showcase your projects, blogs, documentation, or personal websites to the world. Here are some key points about GitHub Pages:</p>
<ol>
<li><p>Free Hosting: GitHub Pages provides free hosting for static websites, which includes HTML, CSS, JavaScript, and other static files. This makes it a cost-effective solution for hosting personal or project websites.</p>
</li>
<li><p>GitHub Integration: GitHub Pages is tightly integrated with GitHub repositories. You can create a dedicated branch (usually named gh-pages or main for user/organization pages) or use the docs folder in your repository to store your website files. When you push changes to this branch or folder, your website is automatically updated.</p>
</li>
</ol>
</blockquote>
<p>This sounded ideal, as I already had a GitHub account and the price was right in my sweet spot (free 🙂).</p>
<p>The original chat can be found <a target="_blank" href="https://chat.openai.com/share/9371adde-efa9-4e74-a8d2-7b0c9a033c2b">here</a>.</p>
<h2 id="heading-how-can-i-test-it">How can I test it?</h2>
<p>Like all good software engineers, my first thought was how to test my static website as I developed it. So I lent on my assistant again:</p>
<blockquote>
<p>I want to develop a static website. Whilst I am doing development on my PC, how can I host the website so that I can see instant results when I save changes?</p>
</blockquote>
<p>As a regular user of VS Code, the following suggestion jumped out:</p>
<blockquote>
<p>Integrated Development Environment (IDE) Servers:</p>
<ol>
<li><p>Most modern Integrated Development Environments (IDEs) or code editors come with built-in tools to preview websites. For instance:</p>
<ul>
<li>Visual Studio Code: Has an extension called Live Server which provides a live preview of your site with hot-reload functionality.</li>
</ul>
</li>
</ol>
</blockquote>
<p>A quick search for the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer">Live Server</a> extension in VS Code and I soon had a way to instantly view rendered HTML. Now I was all set to go into the unfamiliar territory of website development.</p>
<p>The original chat can be found <a target="_blank" href="https://chat.openai.com/share/431c6e0f-74b7-42ad-a981-af10904d6c5b">here</a>.</p>
<h2 id="heading-but-im-not-an-html-expert">But I'm not an HTML expert</h2>
<p>Although I have been in the IT business for quite a while, my focus has pretty much always been on the back end. I have a rudimentary knowledge of HTML, but not much beyond that. What I needed what an expert to help me, so using <a target="_blank" href="https://github.com/paralleldrive/sudolang-llm-support/blob/main/sudolang.sudo.md">SudoLang</a> I prompted ChatGPT with the following:</p>
<pre><code class="lang-markdown"><span class="hljs-section"># An expert in writing HTML</span>

Role-play as a world-class HTML writer who is proficient in using the Bootstrap framework to produce attractive and responsive pages.

HTMLExpert {
  /write - Take a description of a page and output HTML to match it
}
</code></pre>
<p>With the role set, I asked the following:</p>
<blockquote>
<p>/write a page that contains links to an album of MP3 files. The page should contain a placeholder for a description of the album. Along with links to download the MP3 files, there should be the name of each song and a link to a page containing the lyrics for that song.</p>
</blockquote>
<p>The result was an HTML page (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-01.htm">source</a>) that admirably met my specification:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/html-expert-01.png?raw=true" alt="Rendered view of HTML page" /></p>
<p>ChatGPT also added the following, providing helpful assistance in how to use the result:</p>
<blockquote>
<p>To use this template:</p>
<ul>
<li>Replace "Album Name" with the name of your album.</li>
<li>Replace the placeholder text under the ID albumDescription with your album's description.</li>
<li>For each song in your album:<ul>
<li>Duplicate the &lt;li&gt; element.</li>
<li>Replace "Song 1" with the song name.</li>
<li>Replace "path/to/song1.mp3" with the correct path or URL to the MP3 file.</li>
<li>Replace "path/to/song1_lyrics.html" with the correct path or URL to the song's lyrics page.</li>
</ul>
</li>
</ul>
</blockquote>
<p>This looked like a great start, so I wondered if it could be improved:</p>
<blockquote>
<p>That is great. What would be even better would be a music player so that each song could be played directly from the page.</p>
</blockquote>
<p>To which I got the response:</p>
<blockquote>
<p>Of course! Let's integrate an HTML5 &lt;audio&gt; element with controls for each song so that users can play them directly from the page.</p>
<p>Here's an enhanced version of your album page with the music player functionality:</p>
</blockquote>
<p>And sure enough, the page (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-02.htm">source</a>) now had a music player on it. I supplied an MP3 in the appropriate location, and sure enough I could play it from my browser.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/html-expert-02.png?raw=true" alt="Rendered view of the HTML page with a music player" /></p>
<p>I then decided to see if the process would benefit from a more precise specification. So, as an experiment, I tried using <a target="_blank" href="https://www.markdownguide.org/">Markdown</a> as a specification format and prompted ChatGPT as follows:</p>
<blockquote>
<p>/write</p>
<pre><code class="lang-markdown"><span class="hljs-section"># Album Name</span>

A link to an image for the album cover

A list of songs on the album and for each song:

<span class="hljs-bullet">-</span> The title of the song
<span class="hljs-bullet">-</span> A link to download the MP3
<span class="hljs-bullet">-</span> A toggle button to show and hide the notes for the song, the text for the button changes on toggle
<span class="hljs-bullet">-</span> A toggle button to show and hide the lyrics for the song, the text for the button changes on toggle
<span class="hljs-bullet">-</span> An audio player to allow the MP3 to played from the page

The songs are in a sub-folder call mp3s
The images are in a sub-folder called images

The footer should say that all material is copyrighted to Andy Balham
</code></pre>
</blockquote>
<p>The result, once again, was pretty good (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-03.htm">source</a>):</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/html-expert-03.png?raw=true" alt="Rendered page from markdown definition" /></p>
<p>However, testing the page I found that the toggling didn't quite work. Using my programming knowledge I prompted ChatGPT:</p>
<blockquote>
<p>The toggling doesn't work. I think targetElement.classList.contains('show') should be btn.textContent.includes('Hide')).</p>
</blockquote>
<p>To which ChatGPT replied:</p>
<blockquote>
<p>You're right; the toggle behaviour I provided previously was based on Bootstrap's show class state on the target element, which might not reflect the exact moment when the toggle happens. Let's adjust the toggle behaviour by updating the script section.</p>
<p>Here's the updated script:</p>
</blockquote>
<p>And sure enough, an amended script was provided which fixed the issue (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-04.htm">source</a>).</p>
<p>I then spent some time experimenting with refining the SudoLang program to style the page in different ways, before I decided that including the lyrics in the main page would be a bit onerous.</p>
<p>So I asked ChatGPT to regenerate the page with the lyrics details of the page definition updated as follows:</p>
<pre><code class="lang-markdown">For each song:

<span class="hljs-bullet">-</span> A toggle button 'Lyrics' to show and hide the lyrics for the song

Folder information:

<span class="hljs-bullet">-</span> The lyrics are in a sub-folder called lyrics, one file per song
</code></pre>
<p>The result was an amendment to the collapsible lyrics section:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- Lyrics Collapsible Section --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"collapse mt-3"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"lyricsSample"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card card-body"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"lyrics/song-title-lyrics.txt"</span>&gt;</span>View Lyrics<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>A link wasn't what I wanted, so I asked for alternatives:</p>
<blockquote>
<p>Is there any way the text from the lyrics files could be included without using an &lt;a&gt; ref?</p>
</blockquote>
<p>Sure enough ChatGPT responded with some options:</p>
<blockquote>
<p>To include the lyrics text directly on the page without using an &lt;a&gt; link, you have a couple of options, but each comes with its own considerations and limitations:</p>
<ol>
<li>Embedding Lyrics Directly in HTML: &lt;snip&gt;</li>
<li>Using JavaScript to Load Lyrics: &lt;snip&gt;</li>
<li>Server-Side Inclusion: &lt;snip&gt;</li>
</ol>
</blockquote>
<p>The JavaScript option looked like what I wanted, so asked for that:</p>
<blockquote>
<p>Please update the HTML with the JavaScript version</p>
</blockquote>
<p>As you can see from below, the result (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-05.htm">source</a>) was a partial success. The lyrics were loaded on demand, but the rendering left a little to be desired:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/html-expert-05.png?raw=true" alt="Rendered lyrics with no HTML breaks" /></p>
<p>So I asked ChatGPT to sort that out.</p>
<blockquote>
<p>If the lyrics are in plain text format, can you amend the JavaScript to insert suitable HTML markup to make the lyrics break across lines</p>
</blockquote>
<p>The response was an amended JavaScript function, which I then copy and pasted into the page (<a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/html-expert-06.htm">source</a>) and the results are shown below:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/html-expert-06.png?raw=true" alt="Rendered lyrics with HTML breaks" /></p>
<p>The result is not perfect, blank lines have not been preserved, but it will suffice for my needs. Overall, I was impressed with how I was able to go from nothing to a page that contains functionality that I wanted. It won't win any design awards, but it would have taken me significantly longer to develop with the AI assistance.</p>
<p>The whole chat can be found via this <a target="_blank" href="https://chat.openai.com/share/38c54131-b94d-4aea-bc53-bb9896928583">link</a>.</p>
<h2 id="heading-how-could-i-repeat-it">How could I repeat it?</h2>
<p>As a final step, I thought I would lean on ChatGPT once more and ask it to write a JavaScript program to merge the HTML template it had created with a YAML file in the following format:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">albumName:</span> <span class="hljs-string">Aye-Aye?</span>
<span class="hljs-attr">songs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">title:</span> <span class="hljs-string">Guinness</span> <span class="hljs-string">Greed</span>
    <span class="hljs-attr">mp3Filename:</span> <span class="hljs-string">01_guinness_greed</span>
    <span class="hljs-attr">notes:</span> <span class="hljs-string">"Prompt: /genre greed /subject Guinness /write must include reference to John"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">title:</span> <span class="hljs-string">Flood's</span> <span class="hljs-string">Domain</span>
    <span class="hljs-attr">mp3Filename:</span> <span class="hljs-string">02_floods_domain</span>
    <span class="hljs-attr">notes:</span> <span class="hljs-string">"Prompt: /genre fear /subject floods /write"</span>
</code></pre>
<p>My prompt for this was simply the following:</p>
<blockquote>
<p>You are an expert in write Node.js programs. I would like you to write a program that receives a YAML file as input, merges the details of that file with the an HTML template, and then outputs an HTML file containing the merged result.</p>
<p>Here is an example YAML file: &lt;snip&gt;</p>
<p>Here is the HTML template: &lt;snip&gt;</p>
</blockquote>
<p>The resulting output from the resulting <a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/chatgpt-program.js">program</a> was almost perfect:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/output-html-01.png?raw=true" alt="Rendered output HTML showing album name missing" /></p>
<p>The only thing that wasn't right was that the <code>H1</code> element hadn't been replaced with the album name. ChatGPT had used the <code>replace</code> method and that seemed to only replace the first match, so I asked the following:</p>
<blockquote>
<p>How can the program be changed to make replace('Album Name', yamlData.albumName) replace all instances of 'Album Name' and not just the first instance?</p>
</blockquote>
<p>To which I got the following response and an updated <code>mergeYAMLWithTemplate</code> function:</p>
<blockquote>
<p>To replace all instances of a specific string in JavaScript, you can use a regular expression with the global (g) flag. This way, the replace method will replace all occurrences of the string in the text, not just the first one.</p>
</blockquote>
<p>I pasted in the updated function, ran the program and was presented with the following expected result:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/first-ai-assisted-project/output-html-02.png?raw=true" alt="Rendered output HTML showing album name missing" /></p>
<p>The whole chat can be found via this <a target="_blank" href="https://chat.openai.com/share/2c9f3403-2ebf-468a-a3b5-61d8d260a109">link</a> and the JavaScript program <a target="_blank" href="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/files/first-ai-assisted-project/chatgpt-program.js">here</a>.</p>
<h2 id="heading-summary">Summary</h2>
<p>It was a very interesting to try a new process for developing a solution to a problem. At each stage, it was a significant advantage to be able to ask for options on how to solve the next step. The interaction felt very natural, asking for further details or asking for small changes. The process allowed me to explore very quickly and, although I could have written the final program, it was much quicker to get it generated for me. Interestingly, my solution probably would have been at risk of gold-plating. Taking the simple solution has its advantages.</p>
<p>There is no doubt to me that these tools are a significant productivity boost. In the software realm, we can test the results (you do test don't you? 😉). This means we can easily check the output that we are given, which is not the case in others.</p>
<p>The resulting website can be found <a target="_blank" href="https://andybalham.github.io/website-test/">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Step Function Errors Should Be Errors]]></title><description><![CDATA[In this post, we shall see the consequences and limitations of using step function errors as flow control.
See the accompanying GitHub repo for working code examples.
TL;DR

Be wary when using step functions errors for flow control in step functions
...]]></description><link>https://www.10printiamcool.com/step-function-errors-should-be-errors</link><guid isPermaLink="true">https://www.10printiamcool.com/step-function-errors-should-be-errors</guid><category><![CDATA[AWS]]></category><category><![CDATA[stepfunction]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Wed, 29 Nov 2023 18:47:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/52jRtc2S_VE/upload/0acb06d43c79fc94bdf28516a1a9de41.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, we shall see the consequences and limitations of using step function errors as flow control.</p>
<p>See the accompanying <a target="_blank" href="https://github.com/andybalham/blog-errors-should-be-errors">GitHub repo</a> for working code examples.</p>
<h2 id="heading-tldr">TL;DR</h2>
<ul>
<li>Be wary when using step functions errors for flow control in step functions</li>
<li>After throwing and catching an error, only the inputs and the basic error details are available</li>
</ul>
<h2 id="heading-our-inheritance">Our inheritance</h2>
<p>We inherited a codebase that contained a step function which was asynchronously invoked from an API Gateway request. The step function performed validation on the request, before proceeding to processing the request if the validation was successful. When the validation failed, either due to schema errors or due to the content of the request, an error was thrown by the Lambda function. These errors were caught by the step function and, depending on the name of the error, the step function invoked a DynamoDB integration task to update a table with the result. Users of the API could then use another API call to query the state of the request.</p>
<p>A simplified version is shown below:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/error-should-be-errors/state-machine-with-errors.png?raw=true" alt="State machine using errors for flow control" /></p>
<h2 id="heading-improving-things-for-our-users">Improving things for our users</h2>
<p>Users don't always get things right, so it was common for requests to fail. In particular, they would fail for one of the validation reasons. Whilst the underlying reasons for the failures were logged to CloudWatch, it quickly became a pain to keep looking them up whenever a user of the API suffered from a failure.</p>
<p>To provide better feedback for our users, we decided to add the validation errors to the result table. The users could then query this table via the API to understand why their request had failed, and all without bothering us.</p>
<p>The only problem was, at the point the table was being updated, we had no way of accessing the validation failures.</p>
<h2 id="heading-post-error-step-function-context">Post-error step function context</h2>
<p>The problem was that after an error has been handled by a step function, only the following is available:</p>
<ul>
<li>The inputs to the step function</li>
<li>The error type</li>
</ul>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/error-should-be-errors/post-error-input.png?raw=true" alt="Post error step input" /></p>
<p>This makes sense, as there can be no guarantee of the state once an error occurs. The inputs never change, so it is safe to access their values, but it is not safe to access anything else apart from the type of error.</p>
<p>This means that it isn't possible to pass structured information into the error handlers. Which happened to be exactly what we needed to do if we were to extend the DynamoDB integrations to store the extra details.</p>
<h2 id="heading-are-these-really-errors">Are these really errors?</h2>
<p>It is somewhat a matter of opinion, but I would argue that neither validation failure is a true error. I say this, as we would expect both scenarios to occur in normal operations. The use of throwing and catching errors in this case looked to me like it was done as a convenience.</p>
<p>To resolve the issue, the validation Lambda function was updated to add the errors directly to the table. This took advantage of the fact that the relevant item was already present and that the DynamoDB integration would not overwrite any added errors. This allowed us to avoid changing the flow of the step function, but came at the cost of increasing the responsibility of the validation function.</p>
<h2 id="heading-a-better-approach">A better approach</h2>
<p>I would argue that the validation step should produce a validation result.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">interface</span> ValidationResult {  
  user?: User;
  formatErrors?: ZodIssue[]; <span class="hljs-comment">// The errors from using the zod npm package</span>
  contentErrors?: <span class="hljs-built_in">string</span>[];
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validateUser</span>(<span class="hljs-params">event <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span></span>): <span class="hljs-title">ValidationResult</span> </span>{
  <span class="hljs-comment">// &lt;snip&gt;</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;
): <span class="hljs-built_in">Promise</span>&lt;ValidationResult&gt; =&gt; {
  <span class="hljs-keyword">return</span> validateUser(event);
};
</code></pre>
<p>With this, we can then rework the step function with an additional choice step, as shown below:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/error-should-be-errors/state-machine-with-validation-result.png?raw=true" alt="State machine using validation result for flow control" /></p>
<p>Now the DynamoDB integrations can access the relevant details. For example, for format errors.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Key"</span>: {
    <span class="hljs-attr">"key"</span>: {
      <span class="hljs-attr">"S.$"</span>: <span class="hljs-string">"$$.Execution.Input.requestId"</span>
    }
  },
  <span class="hljs-attr">"TableName"</span>: <span class="hljs-string">"&lt;snip&gt;"</span>,
  <span class="hljs-attr">"ExpressionAttributeNames"</span>: {
    <span class="hljs-attr">"#status"</span>: <span class="hljs-string">"status"</span>,
    <span class="hljs-attr">"#formatErrors"</span>: <span class="hljs-string">"formatErrors"</span>
  },
  <span class="hljs-attr">"ExpressionAttributeValues"</span>: {
    <span class="hljs-attr">":status"</span>: {
      <span class="hljs-attr">"S"</span>: <span class="hljs-string">"InvalidFormat"</span>
    },
    <span class="hljs-attr">":formatErrors"</span>: {
      <span class="hljs-attr">"S.$"</span>: <span class="hljs-string">"States.JsonToString($.validationResult.formatErrors)"</span>
    }
  },
  <span class="hljs-attr">"UpdateExpression"</span>: <span class="hljs-string">"SET #status = :status, #formatErrors = :formatErrors"</span>
}
</code></pre>
<p>With this approach, the validation step is pure validation and not given a secondary responsibility of storing any validation errors.</p>
<h2 id="heading-general-discussion">General discussion</h2>
<p>Consider the following C# method:</p>
<pre><code class="lang-c#"><span class="hljs-function">MyEntity <span class="hljs-title">FindMyEntity</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> id</span>)</span>;
</code></pre>
<p>What should the method return if there is no such instance with a matching id? Should it return <code>null</code> or throw an exception?</p>
<p>I would say it depends on the caller. If the caller cannot continue without the entity, throw an exception. If it can continue, return <code>null</code>. This could be catered for with the following:</p>
<pre><code class="lang-c#"><span class="hljs-function">MyEntity <span class="hljs-title">FindMyEntity</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> id, <span class="hljs-keyword">bool</span> throwExceptionIfNotFound = <span class="hljs-literal">false</span></span>)</span>;
</code></pre>
<p>Now the caller can control the behaviour as they need.</p>
<p>You could go further and return a result object.</p>
<pre><code class="lang-c#"><span class="hljs-function">FindResult&lt;MyEntity&gt; <span class="hljs-title">FindMyEntity</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> id</span>)</span>;
</code></pre>
<p>You can then explicitly check for the 'not found' scenario.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">if</span> (entityFindResult.NotFound) <span class="hljs-comment">// ...</span>
<span class="hljs-keyword">else</span> <span class="hljs-comment">// Use entityFindResult.Value</span>
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>I would advise against using errors as a replacement for step function flow control using choice steps. Once the error is thrown, the current state is lost and only the error type is then available downstream.</p>
<p>In general, I would advise against throwing errors/exceptions/&lt;insert your favourite language version here&gt; as part of 'normal' processing. That is, unless the idioms of your preferred language recommend it. I have found that there are usually alternatives that make your code cleaner and more extensible.</p>
]]></content:encoded></item><item><title><![CDATA[My first steps with the future of programming]]></title><description><![CDATA[40 years ago I saw someone try to program with natural language. Now that has become a reality with large language models (LLMs) and SudoLang. My first question was, can we use these tools to generate comedy gold?
40 years ago
It was 1982 and I was a...]]></description><link>https://www.10printiamcool.com/my-first-steps-with-the-future-of-programming</link><guid isPermaLink="true">https://www.10printiamcool.com/my-first-steps-with-the-future-of-programming</guid><category><![CDATA[AI]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[programming languages]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 22 Oct 2023 08:57:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697964872413/c9045cae-202a-43b5-8949-2006f7279143.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>40 years ago I saw someone try to program with natural language. Now that has become a reality with large language models (LLMs) and <a target="_blank" href="https://github.com/paralleldrive/sudolang-llm-support">SudoLang</a>. My first question was, can we use these tools to generate comedy gold?</p>
<h2 id="heading-40-years-ago">40 years ago</h2>
<p>It was 1982 and I was attending the school computer club. We were taking turns to program the 1K <a target="_blank" href="http://www.zx81stuff.org.uk/zx81/jtyone.html">ZX81</a>. One member took their turn at the 'keyboard' and attempted to type something along the lines of...</p>
<pre><code class="lang-text">10 IF THE PLAYER PRESSES LEFT THEN MOVE THE SPACESHIP LEFT
</code></pre>
<p>The result was the following...</p>
<pre><code class="lang-text">10 INPUT F THE PLAYER PRESSES LEFT THEN MOVE THE SPACESHIP LEFT
</code></pre>
<p>Apart from the interesting fact that variables on the ZX81 could have spaces in them, the computer was clearly not going to do what was being asked. How foolish they were, my 13yo self thought smugly to himself.</p>
<p>Fast-forward 40 years and this sort of programming is now possible, thanks to LLMs such as ChatGPT and the insight of <a target="_blank" href="https://medium.com/@_ericelliott">Eric Elliot</a> and his work developing <a target="_blank" href="https://github.com/paralleldrive/sudolang-llm-support/blob/main/sudolang.sudo.md">SudoLang</a>.</p>
<h2 id="heading-encountering-sudolang">Encountering SudoLang</h2>
<p>I was listening to the excellent <a target="_blank" href="https://www.dotnetrocks.com/details/1865">.Net Rocks!</a> podcast, when they mentioned SudoLang. When I mentioned to my co-workers that SudoLang was being developed by Eric Elliot, I was told in no uncertain terms what a very smart fellow he was. So, with my interest sufficiently piqued, I got a ChatGPT Plus subscription, followed the steps in the <a target="_blank" href="https://medium.com/javascript-scene/ai-programming-for-absolute-beginners-16ac3fc6dea6">AI Programming for Absolute Beginners</a> article, and my mind was blown.</p>
<p>To quote from the <a target="_blank" href="https://github.com/paralleldrive/sudolang-llm-support">SudoLang GitHub repo</a>:</p>
<blockquote>
<p>SudoLang is a programming language designed to collaborate with AI language models including ChatGPT, Bing Chat, Anthropic Claude, and Google Bard. It is designed to be easy to learn and use. It is also very expressive and powerful.</p>
<p>All sufficiently advanced language models understand it without any special prompting. You do not need to paste the SudoLang specification before using SudoLang with your favorite AI.</p>
</blockquote>
<h2 id="heading-programming-with-sudolang">Programming with SudoLang</h2>
<p>The question was what sort of program should I write. How about one that generates some comedy names in the spirit of Key and Peele and their <a target="_blank" href="https://www.youtube.com/watch?v=gODZzSOelss">East vs West sketches</a>.</p>
<p>SudoLang programs are written in Markdown and start with the role that provides the context. Think of it like briefing an actor. In our case, an actor that does the following.</p>
<pre><code class="lang-md"><span class="hljs-section"># East vs West Player Generator</span>

Generate a plausible, but comedic sounding name for an American football player.
</code></pre>
<p>We then specify the state of the application. The LLM understands that the curly braces delimit the various components, giving them scope. In SudoLang you lean heavily on what the LLM can infer, so We don't need to specify types were they can be inferred. Where they can't, we provide a hint in a comment such as <code>East|West</code>.</p>
<pre><code class="lang-text">NameGenerator {
  State {
    Name
    CollegeOrSchoolOrPrison
    Team // East|West
    Position
  }
}
</code></pre>
<p>A key concept in SudoLang is programming by constraints, so now we add some to our program.</p>
<pre><code class="lang-text">NameGenerator {
  State {
    ...
  }
  Constraints {
    The name must consist of at least a first name and a surname.
    The name must not be the name of a real player.
    The name must sound amusing, but must be able to be pronounced.
  }
}
</code></pre>
<p>Like the pupil 40 years ago, all we do is express our intention in natural language. In SudoLang, natural language is preferable, as that is what LLMs understand.</p>
<p>Finally, we add a couple of commands and a function to greet the user. Again, we use a convention for commands that the LLM will be familiar with (i.e. <code>/command</code>).</p>
<pre><code class="lang-text">NameGenerator {
  State {
    ...
  }
  Constraints {
    ...
  }
  /next Generate the next name
  /help Provide help
}

welcome("Welcome to the name generator. Type /next to get the next name")
</code></pre>
<p>And now our program ready to run or, if you prefer, our actor is ready to perform.</p>
<p>Pasting it into ChatGPT GPT-4, we get the following response:</p>
<blockquote>
<p>Welcome to the name generator. Type /next to get the next name. If you need assistance or further information, type /help. Enjoy!</p>
</blockquote>
<p>Note here that it has given us the greeting we programmed, but our actor has gone a little further and added that assistance is also available.</p>
<p>Let's try a name, so typing <code>/name</code> returns:</p>
<blockquote>
<p>I'm sorry, the provided command "/name" is not recognized. Please use one of the supported commands such as "/next" or "/help". Would you like to proceed with "/next" to generate a name?</p>
</blockquote>
<p>Oops, I forgot my own commands. However, the program gave a helpful response without us having to explicitly program for that eventuality. Typing <code>/next</code> returns:</p>
<pre><code class="lang-text">I apologize for the confusion. Let me generate a name for you now.

NameGenerator:
  State:
    Name: "Dexter 'Dizzy Dukes' Duckworth"
    CollegeOrSchoolOrPrison: "Puddle Jumpers Academy for Overachievers"
    Team: "East"
    Position: "Running Back"
</code></pre>
<p>Our actor is almost British here, apologising for other's mistakes. However, it has successfully done what we wanted and generated a mildly amusing name, plus a manufactured college of sorts. Not exactly comedy gold, but not too bad.</p>
<h2 id="heading-refining-the-program">Refining the program</h2>
<p>To refine our generator, let's add another line to the context to try to get names more like Key and Peele.</p>
<pre><code class="lang-text"># East vs West Player Generator
...
The name can have a middle name or initial and can have a suffix, such as III.
</code></pre>
<p>We'll also add a <code>height</code> and <code>weight</code> to the state. We will leave it to the LLM to work out suitable units.</p>
<pre><code class="lang-text">State {
  ...
  Weight
  Height
}
</code></pre>
<p>The colleges in the Key and Peele sketch are real ones, so let's add a constraint to ensure that ours are too. While we are at it, we add a constraint to make sure our offensive linemen are suitably hefty.</p>
<pre><code class="lang-text">Constraints {
  ...
  The college, school, or prison must be real.
  The weight and height must be consistent with their position in the team
}
</code></pre>
<p>Finally, we add a <code>/refine</code> command and amend the <code>/next</code> to output <a target="_blank" href="https://en.wikipedia.org/wiki/YAML">YAML</a>.</p>
<pre><code class="lang-text">/next Generate the next name and output the result in YAML
...
/refine Generate another name based on the last, but with more exaggeration
</code></pre>
<p>OK. When we paste in the program we get:</p>
<blockquote>
<p>Welcome to the name generator. Type /next to get the next name. For assistance or further options, type /help. Let's create some memorable players!</p>
</blockquote>
<p>Once more, our actor has taken some liberties with the script, but the gist is spot on. Let's try out the <code>/next</code> command.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NameGenerator:</span>
  <span class="hljs-attr">State:</span>
    <span class="hljs-attr">Name:</span> <span class="hljs-string">"Thaddeus T. 'Thunder Thighs' Thompson III"</span>
    <span class="hljs-attr">CollegeOrSchoolOrPrison:</span> <span class="hljs-string">"University of Alabama"</span>
    <span class="hljs-attr">Team:</span> <span class="hljs-string">"West"</span>
    <span class="hljs-attr">Position:</span> <span class="hljs-string">"Defensive Lineman"</span>
    <span class="hljs-attr">Weight:</span> <span class="hljs-string">"310 lbs"</span>
    <span class="hljs-attr">Height:</span> <span class="hljs-string">"6'4''"</span>
</code></pre>
<p>Success. We have our output in YAML, the college is real, and the height and weight are position-appropriate. Let's try <code>/refine</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NameGenerator:</span>
  <span class="hljs-attr">State:</span>
    <span class="hljs-attr">Name:</span> <span class="hljs-string">"Thaddeus T. 'Titanium Tackle' Thompson IV"</span>
    <span class="hljs-attr">CollegeOrSchoolOrPrison:</span> <span class="hljs-string">"University of Alabama"</span>
    <span class="hljs-attr">Team:</span> <span class="hljs-string">"West"</span>
    <span class="hljs-attr">Position:</span> <span class="hljs-string">"Defensive Lineman"</span>
    <span class="hljs-attr">Weight:</span> <span class="hljs-string">"325 lbs"</span>
    <span class="hljs-attr">Height:</span> <span class="hljs-string">"6'5''"</span>
</code></pre>
<p>OK. Not the greatest exaggeration perhaps, but the program did do what we asked.</p>
<p>Let's try one final revision of our program. First let's give each player a catchphrase.</p>
<pre><code class="lang-text"># East vs West Player Generator
...
Each player has a catchphrase based on their background, position,
  and physical stature.

NameGenerator {
  State {
    ...
    Catchphrase
  }
  Constraints {
    ...
    The weight and height must be consistent with their position in the team,
      but can be exaggerated for comic purposes.
    The catchphrase must not be based on any stereotypes.
  }
  ...
  /opposite Generate another name based on the last,
    but with opposite characteristics
}

overTheTopWelcomeInTheStyleOfAFootballCoach()
</code></pre>
<p>We have also loosened the height and weight constraint to try to get more extreme values and added a new command to generate an opposite character. The welcome was good before, but it could be more in keeping with the subject matter. So let's see if the amended version can gee us up a bit.</p>
<blockquote>
<p>Alright, team! Gather 'round! 🏈 This ain't just any old name generator! We're here to scout out the funniest, quirkiest, and downright unforgettable players! You want comedy? We got it! You want star power? We got it! Strap on those helmets, get in formation, and type "/next" to discover the next legendary player! Remember, we play hard, we play fair, but most importantly, we play for laughs! LET'S GO TEAM! 📣🎉</p>
</blockquote>
<p>Yes, that certainly had an effect. Let's do what coach says and type <code>/next</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NameGenerator:</span>
  <span class="hljs-attr">State:</span>
    <span class="hljs-attr">Name:</span> <span class="hljs-string">"Fitzgerald F. 'Flashy Footwork' Fiddlesticks"</span>
    <span class="hljs-attr">CollegeOrSchoolOrPrison:</span> <span class="hljs-string">"Harvard University"</span>
    <span class="hljs-attr">Team:</span> <span class="hljs-string">"East"</span>
    <span class="hljs-attr">Position:</span> <span class="hljs-string">"Wide Receiver"</span>
    <span class="hljs-attr">Weight:</span> <span class="hljs-string">"185 lbs"</span>
    <span class="hljs-attr">Height:</span> <span class="hljs-string">"6'2''"</span>
    <span class="hljs-attr">Catchphrase:</span> <span class="hljs-string">"You can't catch what you can't see!"</span>
</code></pre>
<p>Not bad, let's try the <code>/opposite</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NameGenerator:</span>
  <span class="hljs-attr">State:</span>
    <span class="hljs-attr">Name:</span> <span class="hljs-string">"Barnaby B. 'Boulder Boot' Bumblebee"</span>
    <span class="hljs-attr">CollegeOrSchoolOrPrison:</span> <span class="hljs-string">"Stanford University"</span>
    <span class="hljs-attr">Team:</span> <span class="hljs-string">"West"</span>
    <span class="hljs-attr">Position:</span> <span class="hljs-string">"Defensive Tackle"</span>
    <span class="hljs-attr">Weight:</span> <span class="hljs-string">"320 lbs"</span>
    <span class="hljs-attr">Height:</span> <span class="hljs-string">"5'10''"</span>
    <span class="hljs-attr">Catchphrase:</span> <span class="hljs-string">"I'm the mountain you can't move!"</span>
</code></pre>
<p>Now let's go off-piste and just ask <code>Please generate a punter from a prison</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NameGenerator:</span>
  <span class="hljs-attr">State:</span>
    <span class="hljs-attr">Name:</span> <span class="hljs-string">"Percival P. 'Penitentiary Punt' Peterson"</span>
    <span class="hljs-attr">CollegeOrSchoolOrPrison:</span> <span class="hljs-string">"San Quentin State Prison"</span>
    <span class="hljs-attr">Team:</span> <span class="hljs-string">"West"</span>
    <span class="hljs-attr">Position:</span> <span class="hljs-string">"Punter"</span>
    <span class="hljs-attr">Weight:</span> <span class="hljs-string">"195 lbs"</span>
    <span class="hljs-attr">Height:</span> <span class="hljs-string">"6'0''"</span>
    <span class="hljs-attr">Catchphrase:</span> <span class="hljs-string">"From the yard to the 50-yard line!"</span>
</code></pre>
<p>This highlights that our programs can do more than we explicitly specify. So let's push that a bit further.</p>
<h2 id="heading-using-the-llm-to-improve-our-program">Using the LLM to improve our program</h2>
<p>Now that the LLM knows is very familiar with the context we are dealing with, let's see if it can suggest ways we can improve the program. Typing <code>Please suggest some possible commands to help refine the comedy potential of the characters</code> gets us the following.</p>
<blockquote>
<p>Certainly! Here are some potential commands to help refine the comedy potential:</p>
<ol>
<li><code>/exaggerate</code>: Amp up the comedy by making a character's feature or trait more extreme.</li>
<li><code>/mashup</code>: Combine elements of two or more previously generated characters.</li>
<li><code>/nickname</code>: Generate a unique and funny nickname based on the character's existing traits.</li>
<li><code>/backstory</code>: Provide a short, humorous backstory for the character that ties into their attributes or catchphrase.</li>
<li><code>/random_trait</code>: Add a random and unexpected trait or hobby that contrasts with the character's football position or personality for comedic effect.</li>
<li><code>/swap</code>: Swap one trait or attribute for its comedic opposite.</li>
<li>...and another six</li>
</ol>
<p>These commands would help add layers of comedy and depth to each character, making them more memorable and entertaining.</p>
</blockquote>
<p>Not bad suggestions, let's add them to our program and try out <code>/backstory</code> for 'Bubba "Biscuit" McGillicuddy IV'.</p>
<blockquote>
<p>Bubba "Biscuit" McGillicuddy IV hails from the small town of Butterville, a place famous for its annual Butter Festival. Born into the McGillicuddy family, owners of the renowned "McGillicuddy's Marvelous Muffins &amp; More" bakery, Bubba was always the center of attention.</p>
<p>Growing up, while other kids played with toys, young Bubba played with dough. In fact, his nickname "Biscuit" was coined when, at the age of seven, he mistakenly wore a biscuit mold as a helmet and ran head-first into a wall, leaving a biscuit-shaped indentation. The name stuck, and so did his love for the sport.</p>
<p>...and so on.</p>
</blockquote>
<p>This got some genuine laughs when I read it out, so I consider our quest for humour successful.</p>
<h2 id="heading-summary">Summary</h2>
<p>SudoLang has changed the way I look at interacting with LLMs such as ChatGPT. The genius of SudoLang is that it takes advantage of the conventions that the LLMs have learnt, such a <code>/commands</code>, interfaces, and <code>{ scoping }</code>. I now approach each interaction with ChatGPT as if I am approaching an expert who I need to brief on what I want, what I don't want, and how I want it. SudoLang gives me a way to express this in a structured way. Once the expert has some experience of what I am after, I can even ask the expert how I can improve on what I am asking for.</p>
<p>I feel the same excitement of possibilities as I did when I got my own ZX81 40 years ago. But now, of course, I can use natural language.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><a target="_blank" href="https://medium.com/javascript-scene/ai-programming-for-absolute-beginners-16ac3fc6dea6">AI Programming for Absolute Beginners: Your First Steps with SudoLang</a></li>
<li><a target="_blank" href="https://github.com/paralleldrive/sudolang-llm-support/blob/main/sudolang.sudo.md">SudoLang on GitHub</a></li>
<li><a target="_blank" href="https://medium.com/javascript-scene/sudolang-a-powerful-pseudocode-programming-language-for-llms-d64d42aa719b">SudoLang: A Powerful Pseudocode Programming Language for LLMs</a></li>
<li><a target="_blank" href="https://medium.com/javascript-scene/anatomy-of-a-sudolang-program-prompt-engineering-by-example-f7a7b65263bc">Anatomy of a SudoLang Program: Prompt Engineering by Example</a></li>
<li><a target="_blank" href="https://chat.openai.com/share/1488c408-8430-454f-84b8-fdd1d8f815a2">Learn SudoLang on ChatGPT</a></li>
<li><a target="_blank" href="https://medium.com/javascript-scene/the-art-of-effortless-programming-3e1860abe1d3">The Art of Effortless Programming: Why Every Developer Should Learn ChatGPT and SudoLang</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=2jqPJsPuf9E">AI Driven Development with SudoLang - Autodux (YouTube video)</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Adventures with AWS X-Ray and CDK - Part 3]]></title><description><![CDATA[In this post I continue my adventures with X-Ray and try my hand at observing a whole application. In the previous posts in the series, I looked at using X-Ray in a small context. Here we will see what happens when an end-to-end process is traced and...]]></description><link>https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-3</link><guid isPermaLink="true">https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-3</guid><category><![CDATA[aws x-ray]]></category><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[stepfunction]]></category><category><![CDATA[eventbridge]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 24 Sep 2023 18:38:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Q1p7bh3SHj8/upload/8fb6a0249fde7235fb7249a66d12472d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post I continue my <a target="_blank" href="https://www.10printiamcool.com/series/adventures-with-xray">adventures with X-Ray</a> and try my hand at observing a whole application. In the previous posts in the <a target="_blank" href="https://www.10printiamcool.com/series/adventures-with-xray">series</a>, I looked at using X-Ray in a small context. Here we will see what happens when an end-to-end process is traced and logged.</p>
<p>You can download the accompanying code and try it yourself from the <a target="_blank" href="https://github.com/andybalham/blog-enterprise-integration/releases/tag/blog-add-xray">GitHub repo</a>.</p>
<h2 id="heading-the-example-application">The example application</h2>
<p>The case study we looked at in the series on <a target="_blank" href="https://www.10printiamcool.com/series/enterprise-patterns-cdk">implementing Enterprise Integration patterns</a> is an application that acts as a loan broker. The application in question receives a request containing the details of the loan required via an API, and then returns the best rate to a <a target="_blank" href="https://www.getvero.com/resources/webhooks/">webhook</a>.</p>
<p>The following diagram shows how we use a central <a target="_blank" href="https://aws.amazon.com/eventbridge/">EventBridge</a> event bus to implement this.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/case-study-eventbridge.png?raw=true" alt="Architecture diagram using EventBridge" /></p>
<h2 id="heading-view-the-service-map">View the service map</h2>
<p>In the <a target="_blank" href="https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-2?source=more_series_bottom_blogs">last post</a>, I walked through how I added X-Ray to the whole application. Now when I run some requests through the API, we see the following service map.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/service-map-overview.png?raw=true" alt="Service map showing end-to-end application" /></p>
<p>What is quite clear from this picture, is that events are at the heart of this application.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/service-map-events-and-callback.png?raw=true" alt="Focus on events at the heart of the service map" /></p>
<p>Now, by clicking on the client, we can trace a request all the way through the application and out to the webhook.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/service-map-request-trace.png?raw=true" alt="Service map showing how to view traces from the client" /></p>
<p>However, when I tried this, I found something that was hindering the observability. Ironically, it was observability that I added in the post on <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-domain-observability">Domain Observability</a>.</p>
<h2 id="heading-removing-observability-from-tracing">Removing observability from tracing</h2>
<p>In that <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-domain-observability">post</a>, I added business-level observability by hooking a Lambda function up to the domain events being raised.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/observer.png?raw=true" alt="Architecture diagram showing the observability Lambda function" /></p>
<p>However, as I had enabled tracing for this Lambda function, the trace included numerous entries for the observers which clouded the picture of the process.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/observability-trace.png?raw=true" alt="Trace showing how observability is being noisy" /></p>
<p>When viewing the log, this was further apparent.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/trace-log-with-observers.png?raw=true" alt="Log showing how observability is being noisy" /></p>
<p>The solution is to specify <code>Tracing.DISABLED</code> for the observability Lambda functions. However, as I still wanted the traces when testing the Lambda functions, I added a <code>isTestMode</code> property the observability <a target="_blank" href="https://aws.amazon.com/cdk/">CDK</a> stack as follows.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> loggerFunction = <span class="hljs-keyword">new</span> NodejsFunction(
  <span class="hljs-built_in">this</span>,
  <span class="hljs-string">'Logger'</span>,
  getNodejsFunctionProps({
    <span class="hljs-comment">// We don't want the observing in the trace for production</span>
    tracing: props.isTestMode ? Tracing.ACTIVE : Tracing.DISABLED,
    <span class="hljs-comment">// &lt;snip&gt;</span>
  })
);
</code></pre>
<p>Now production traces are clean, but we can also take advantage of X-Ray when testing the functionality.</p>
<h2 id="heading-adding-custom-subsegments">Adding custom subsegments</h2>
<p>The article <a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html">Generating custom subsegments with the X-Ray SDK for Node.js</a> describes subsegments as follows.</p>
<blockquote>
<p>Subsegments extend a trace's segment with details about work done in order to serve a request. Each time you make a call with an instrumented client, the X-Ray SDK records the information generated in a subsegment. You can create additional subsegments to group other subsegments, to measure the performance of a section of code, or to record annotations and metadata.</p>
</blockquote>
<p>In our application, we have Lambda functions that simulate response from lender systems. At the moment, this is just an algorithm, but in practise would be a call that would take time and be prone to error. This would be an ideal call to surround with a custom subsegment.</p>
<p>With this in mind, I added the following code to allow the lender configuration to control the delay in responding and whether an error occurred.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> simulateExternalCallAsync = <span class="hljs-keyword">async</span> (
  lenderConfig: LenderConfig
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
  <span class="hljs-keyword">const</span> randomPercentage = randomInt(<span class="hljs-number">100</span>);
  <span class="hljs-keyword">const</span> errorPercentage = lenderConfig.errorPercentage ?? <span class="hljs-number">0</span>;
  <span class="hljs-keyword">const</span> throwError = randomPercentage &lt;= errorPercentage;

  <span class="hljs-keyword">const</span> delayMillis = lenderConfig.minDelayMillis ?? <span class="hljs-number">1000</span> + randomInt(<span class="hljs-number">1000</span>);
  <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, delayMillis));

  <span class="hljs-keyword">if</span> (throwError) {
    <span class="hljs-keyword">const</span> errorMessage = <span class="hljs-string">`Simulated error (<span class="hljs-subst">${randomPercentage}</span> &lt;= <span class="hljs-subst">${errorPercentage}</span>)`</span>;
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(errorMessage);
  }
};
</code></pre>
<p>With this in place, I added the code below around the call to <code>simulateExternalCallAsync</code> to add and close the subsegment.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> AWSXRay <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-xray-sdk'</span>;

<span class="hljs-comment">// &lt;snip&gt;</span>

<span class="hljs-keyword">const</span> segment = AWSXRay.getSegment();
<span class="hljs-keyword">const</span> subsegment = segment?.addNewSubsegment(<span class="hljs-string">'External Call'</span>);

<span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// Simple values that are indexed for filter expressions</span>
  subsegment?.addAnnotation(<span class="hljs-string">'callType'</span>, <span class="hljs-string">'Lender'</span>);
  subsegment?.addAnnotation(<span class="hljs-string">'lenderId'</span>, lenderConfig.lenderId);
  <span class="hljs-comment">// Related data for debugging purposes</span>
  subsegment?.addMetadata(<span class="hljs-string">'lenderDetails'</span>, {
    lenderId: lenderConfig.lenderId,
    lenderName: lenderConfig.lenderName,
    lenderUrl: <span class="hljs-string">`https://<span class="hljs-subst">${lenderConfig.lenderId}</span>.com`</span>,
  });

  <span class="hljs-keyword">await</span> simulateExternalCallAsync(lenderConfig);

} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-keyword">if</span> (error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span>) {
    <span class="hljs-comment">// Add error to the subsegment</span>
    subsegment?.addError(error);
  }
  <span class="hljs-keyword">throw</span> error;
} <span class="hljs-keyword">finally</span> {
  <span class="hljs-comment">// Ensure the subsegment is closed</span>
  subsegment?.close();
}
</code></pre>
<p>I redeployed the lenders, with the configuration set to introduce delays but not throw any errors. After running a request, I could see the following in the trace.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/custom-segment-traces.png?raw=true" alt="Trace showing custom segment" /></p>
<p>So we can now see the time taken for our 'external call'. What we can also see is the annotation and metadata that we added to the subsegment. Annotations are key-value pairs with simple data (strings, numbers, or booleans) that are indexed for use with filter expressions, whilst metadata can be any related data you'd like to store that's not indexed.</p>
<p>Clicking on the 'Annotations' tab of the segment details, we can see what type of call it was and which lender was called.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/custom-segment-annotations.png?raw=true" alt="Trace showing custom segment annotations" /></p>
<p>For more data, clicking on 'Metadata' shows the lender name and the URL being 'called'.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/custom-segment-metadata.png?raw=true" alt="Trace showing custom segment metadata" /></p>
<h2 id="heading-forcing-some-errors">Forcing some errors</h2>
<p>After having a look at what a happy system looks like, I decided to introduce some errors. I updated the configuration for one of the lenders so that it would always throw an error and redeployed.</p>
<p>After running a request through the system, the service map clearly showed that the Lambda function for the lender has errors.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/error-service-map.png?raw=true" alt="Service map showing Lambda function error" /></p>
<p>Looking at the trace, I could see that the asynchronous request from the step function had timed out, as the timeout was set to 12 seconds.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/error-step-function-timeout.png?raw=true" alt="Trace showing the step function timeout" /></p>
<p>The trace also contained the custom subsegment, which clearly shows that there was an error making the external call.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/error-segment-trace.png?raw=true" alt="Trace showing custom segment failure" /></p>
<p>As before, the 'Annotations' and 'Metadata' tabs showed the call details. However, as we added the error to the subsegment, the 'Exceptions' tab also shows us the error details, including the stack trace.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/error-segment-exception.png?raw=true" alt="Trace showing custom segment exception" /></p>
<p>This shows the power of using subsegments to instrument key parts of the application.</p>
<h2 id="heading-running-a-workload">Running a workload</h2>
<p>The final part of my adventure with X-Ray involved configuring the Lambda functions for the lenders to randomly error and then to put multiple requests through the application.</p>
<p>Looking at the resulting service map, I could see some errors I was expecting and some I didn't.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/running-system-service-map.png?raw=true" alt="Service map showing a workload running with errors" /></p>
<p>In particular, the step function was showing that it had errored. So I selected it and filtered the traces to see what was going on. On inspection, the Lambda function that sends the response to the webhook was erroring. Clicking on the 'Exceptions' tab clearly indicated to me that the issue was that there was no response to send in this case, but the code didn't cater for it.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/running-system-bad-logic.png?raw=true" alt="Trace showing an error due to bad logic" /></p>
<p>Looking at another error, I saw that the Lambda function that looks up the lenders from the <a target="_blank" href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html">parameter store</a> is throwing an error. Again, the 'Exceptions' tab shows the underlying reason. In this case, there is a rate limit on accessing the parameters in the parameter store. This indicates to me that perhaps we need the application to implement some sort of cache in front of the raw access.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-3/running-system-rate-exceeded.png?raw=true" alt="Trace showing an error due to rate limiting" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, I documented my experience using X-Ray with an application. I found the ability to view individual traces through the application, along with the associated logs, very valuable indeed. The ability to see errors and then drill down to the causes was also very valuable and allowed me to see some behaviour that otherwise would have been tricky to spot. Overall, I was very impressed as to what X-Ray has to offer.</p>
<p>Of course, there are also numerous excellent third-party offerings, such as the following:</p>
<ul>
<li><a target="_blank" href="https://newrelic.com/">New Relic</a></li>
<li><a target="_blank" href="https://www.datadoghq.com/">Datadog</a></li>
<li><a target="_blank" href="https://lumigo.io/">Lumigo</a></li>
<li><a target="_blank" href="https://www.thundra.io/">Thundra</a></li>
</ul>
<p>So, although you get X-Ray out of the box with AWS, please consider these as well.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><a target="_blank" href="https://github.com/andybalham/blog-enterprise-integration/releases/tag/blog-add-xray">Accompanying GitHub repo</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-services.html">Integrating AWS X-Ray with other AWS services</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=OOScvywKj9s">How to do Distributed tracing in AWS? | AWS X-ray and Cloudwatch Service Lens</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_xray-readme.html">aws-cdk-lib.aws_xray module</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html">Tracing AWS SDK calls with the X-Ray SDK for Node.js</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=OOScvywKj9s">How to do Distributed tracing in AWS? | AWS X-ray and Cloudwatch Service Lens</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html">Using AWS Lambda with AWS X-Ray</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/API_TracingConfig.html">X-Ray Tracing Modes</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-enabling-xray.html">Setting up AWS X-Ray with API Gateway REST APIs</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Adventures with AWS X-Ray and CDK - Part 2]]></title><description><![CDATA[In the first part of this series, I added AWS X-Ray to a set of examples for my CDK Cloud Test Kit. In this part, I look at adding it to an example application that I put together for my series on implementing Enterprise Integration patterns. Let's s...]]></description><link>https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-2</link><guid isPermaLink="true">https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-2</guid><category><![CDATA[AWS]]></category><category><![CDATA[stepfunction]]></category><category><![CDATA[xray]]></category><category><![CDATA[CDK]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Tue, 29 Aug 2023 07:01:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UN4PadDppAU/upload/ec2665e7fe93c6c35898b182d1d93239.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-1">first part</a> of this series, I added <a target="_blank" href="https://aws.amazon.com/xray/">AWS X-Ray</a> to a set of examples for my <a target="_blank" href="https://www.npmjs.com/package/@andybalham/cdk-cloud-test-kit">CDK Cloud Test Kit</a>. In this part, I look at adding it to an example application that I put together for my series on <a target="_blank" href="https://www.10printiamcool.com/series/enterprise-patterns-cdk">implementing Enterprise Integration patterns</a>. Let's see what adventures I have.</p>
<h2 id="heading-the-example-application">The example application</h2>
<p>The case study we looked at in the series on <a target="_blank" href="https://www.10printiamcool.com/series/enterprise-patterns-cdk">implementing Enterprise Integration patterns</a> is an application that acts as a loan broker. The application in question receives a request containing the details of the loan required via an API, and then returns the best rate to a <a target="_blank" href="https://www.getvero.com/resources/webhooks/">webhook</a>.</p>
<p>The following diagram shows how we use a central <a target="_blank" href="https://aws.amazon.com/eventbridge/">EventBridge</a> event bus to implement this.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/case-study-eventbridge.png?raw=true" alt="Architecture diagram using EventBridge" /></p>
<h2 id="heading-adding-x-ray">Adding X-Ray</h2>
<p>Adding X-Ray to the application involved the following steps.</p>
<ol>
<li>Enable tracing via CDK<ul>
<li>Lambda functions</li>
<li>Step functions</li>
</ul>
</li>
<li>Wrap SDK clients<ul>
<li>EventBridge</li>
</ul>
</li>
</ol>
<p>Enabling tracing on the Lambda functions was as straightforward as adding the following line to the default properties for all Lambda functions in the application.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> NODE_DEFAULT_PROPS = {
  <span class="hljs-comment">// &lt;snip&gt;</span>
  tracing: Tracing.ACTIVE,
};
</code></pre>
<p>The only place I chose to override this default behaviour was in the API handler. Here I used <code>Tracing.PASS_THROUGH</code>, so that it would adhere to the upstream sampling set in the API. See the following StackOverflow post <a target="_blank" href="https://stackoverflow.com/questions/64800794/what-is-active-tracing-mean-in-lambda-with-xray">What is <code>Active tracing</code> mean in lambda with Xray?</a> for a good explanation on what the tracing levels mean.</p>
<p>The application only uses one step function and so it was amended directly as follows.</p>
<pre><code class="lang-TypeScript"><span class="hljs-built_in">this</span>.stateMachine = <span class="hljs-keyword">new</span> StateMachine(<span class="hljs-built_in">this</span>, <span class="hljs-string">'StateMachine'</span>, {
  tracingEnabled: <span class="hljs-literal">true</span>,
  <span class="hljs-comment">// &lt;snip&gt;</span>
});
</code></pre>
<p>The final step was to wrap all the SDK clients that make requests via passive services, such as EventBridge or SQS.</p>
<p>As was shown in the diagram above, all communication in the application is done through EventBridge. In fact, all Lambda functions use the same <code>putDomainEventAsync</code> method to send domain events.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> putDomainEventAsync = <span class="hljs-keyword">async</span> &lt;T <span class="hljs-keyword">extends</span> Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;&gt;({
  eventBusName,
  domainEvent,
}: {
  eventBusName?: <span class="hljs-built_in">string</span>;
  domainEvent: DomainEvent&lt;T&gt;;
}): <span class="hljs-built_in">Promise</span>&lt;AWS_EventBridge.PutEventsCommandOutput&gt; =&gt; {
  <span class="hljs-comment">// &lt;snip&gt;</span>
};
</code></pre>
<p>The upshot of this is that there was only one place to wrap the EventBridge SDK client:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> eventBridge = AWSXRay.captureAWSv3Client(<span class="hljs-keyword">new</span> EventBridge({}));
</code></pre>
<p>And with this, I had added X-Ray to the whole application.</p>
<h2 id="heading-step-functions-and-eventbridge">Step Functions and EventBridge</h2>
<p>One thing that I was aware of, was that the step function uses <code>EventBridgePutEvents</code> direct integration, as highlighted below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/example-application-step-function-put-events.png?raw=true" alt="Step function diagram highlighting the put events tasks" /></p>
<p>I was asking myself if this call would be traced by X-Ray. To test if this is the case, I decided to run one of the the unit tests that executes the step function.</p>
<h2 id="heading-the-unit-test">The unit test</h2>
<p>The unit test differs from a more traditional unit test in that it exercises the step function in the cloud as part of an ephemeral, test-specific CDK stack. This approach allows the Lambda functions used by the step function to be swapped for test-specific implementations. This allows us to script responses for those function and so test all the routes through the step function. For an in-depth look at the approach, please see my post <a target="_blank" href="https://www.10printiamcool.com/step-function-integration-testing-with-cdk">Step Function integration testing with CDK</a>.</p>
<p>The first test I tried initiates the step function by publishing a <code>quoteSubmitted</code> event. It then waits for a Lambda function, acting as a test observer, to receive a <code>quoteProcessedEvent</code>.</p>
<pre><code class="lang-TypeScript"><span class="hljs-comment">// Act</span>

<span class="hljs-keyword">await</span> putDomainEventAsync({
  eventBusName: loanBrokerEventBus.eventBusArn,
  domainEvent: quoteSubmitted,
});

<span class="hljs-comment">// Await</span>

<span class="hljs-keyword">const</span> { observations: quoteProcessedObservations, timedOut } =
  <span class="hljs-keyword">await</span> testClient.pollTestAsync({
    filterById: LoanBrokerTestStack.QuoteProcessedObserverId,
    until: <span class="hljs-keyword">async</span> (o) =&gt; o.length &gt; <span class="hljs-number">0</span>,
    timeoutSeconds: <span class="hljs-number">30</span>,
  });

<span class="hljs-comment">// Assert</span>

expect(timedOut).toBeFalsy();
</code></pre>
<p>However, when I ran the test, I found that it was timing out waiting for the expected event.</p>
<h2 id="heading-why-is-my-test-failing">Why is my test failing?</h2>
<p>As I was now using X-Ray, I had a look at the service map.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-with-failures-map.png?raw=true" alt="A service map showing services with errors" /></p>
<p>On the up side, I could see that the <code>EventBridgePutEvents</code> step function task does allow events to be traced through EventBridge. On the down side, I could see that there were failures in three places, the step function and two Lambda functions.</p>
<p>Looking at the trace for the step function, I could see that the 'RequestCreditReport' task was failing just after 6 seconds. The timeout for this task was set to 6 seconds, so this looked like the task was probably timing out. That would certainly explain why the expected event was not being published.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-step-function-failures-trace.png?raw=true" alt="X-Ray trace shown step function failures" /></p>
<p>I could also see that the Lambda function that provides mock credit references was failing. This would explain why the step function task was timing out, as the step function task was never receiving an event back.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-mock-bureau-failures-trace.png?raw=true" alt="X-Ray trace showing errors in a Lambda function" /></p>
<p>The invocation duration of 2.99s also looked like a timeout, as the timeout for Lambda functions was set to the default of 3 seconds. The console allowed me to quickly dive into the logs and confirm that that was the case.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-mock-bureau-timeout-log.png?raw=true" alt="A CloudWatch log showing errors in a Lambda function" /></p>
<p>Looking at the trace for the other failure, I could see that three attempts were made to the Lambda function that handles callbacks to the step function.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-callback-retries-trace.png?raw=true" alt="X-Ray trace showing multiple retries failing" /></p>
<p>Again, I was easily able to navigate to the logs and see the reason.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-callback-retry-log.png?raw=true" alt="A CloudWatch log showing the details of retry failures" /></p>
<p>I could see that the Lambda function is being invoked as the result of an EventBridge rule. The Lambda function is then trying to restart the step function, but the step function to be restarted has already finished due to the task timeout. The result is an error, which then causes EventBridge to retry.</p>
<h2 id="heading-the-solution">The solution</h2>
<p>The solution to the timeouts was quite simply to double the memory of the highlighted Lambda functions to 256mb and double the timeout to 6 seconds. With these changes in place and deployed, the unit test ran successfully and the resulting service map reflect the clean run.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-map.png?raw=true" alt="A service map showing no errors" /></p>
<p>This service map clearly shows how EventBridge is at the heart of our application. When I selected a successful trace, I could see all the relevant logs in one place.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-2/test-success-log.png?raw=true" alt="A CloudWatch log showing all log entries for the X-Ray trace" /></p>
<h2 id="heading-summary">Summary</h2>
<p>Although I started this post with the intention of diving into the traces of a full application, that will have to wait until the next post.</p>
<p>This short exercise with the unit test shows how you can use the service map and traces to see where issues are. The integration with the logs then allows you to drill down and see the underlying reasons. In particular, X-Ray gave visibility to the asynchronous event-driven behaviour and the behaviour under failure conditions.</p>
<p>I am looking forward to seeing what tracing through the application will bring.</p>
<h2 id="heading-addendum">Addendum</h2>
<p>The unit tests used a wrapped SDK client to publish events. This cause the following error to appear in the console.</p>
<pre><code class="lang-text">console.error
  2023-08-19 14:28:41.709 +01:00 [ERROR] Error: Failed to get the current sub/segment from the context.
      at Object.contextMissingLogError [as contextMissing] (D:\Users\andyb\Documents\github\blog-enterprise-integration\node_modules\aws-xray-sdk-core\dist\lib\context_utils.js:22:27)
      &lt;snip&gt;
      at Object.&lt;anonymous&gt; (D:\Users\andyb\Documents\github\blog-enterprise-integration\tests\loan-broker\loan-broker.test.ts:178:5)
</code></pre>
<p>As mention in <a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-configuration.html">Configuring the X-Ray SDK for Node.js</a>, the solution was to add <code>AWS_XRAY_CONTEXT_MISSING=IGNORE_ERROR</code> in my <code>.env</code> file.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-services.html">Integrating AWS X-Ray with other AWS services</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=OOScvywKj9s">How to do Distributed tracing in AWS? | AWS X-ray and Cloudwatch Service Lens</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-awssdkclients.html">Tracing AWS SDK calls with the X-Ray SDK for Node.js</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=OOScvywKj9s">How to do Distributed tracing in AWS? | AWS X-ray and Cloudwatch Service Lens</a></li>
<li>For Lambda<ul>
<li><a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html">Using AWS Lambda with AWS X-Ray</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/API_TracingConfig.html">X-Ray Tracing Modes</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Adventures with AWS X-Ray and CDK - Part 1]]></title><description><![CDATA[One of the big challenges of distributed systems, such as serverless, is observability. In previous posts, I had a look at structured logging, metrics, and alarms. All these play an important part in building a maintainable system. One thing that I h...]]></description><link>https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-1</link><guid isPermaLink="true">https://www.10printiamcool.com/adventures-with-aws-x-ray-and-cdk-part-1</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[aws x-ray]]></category><category><![CDATA[aws lambda]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Tue, 08 Aug 2023 18:13:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/RkBTPqPEGDo/upload/8436ecc8ead559fadcabfdb5161d5b46.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the big challenges of distributed systems, such as serverless, is observability. In previous posts, I had a look at <a target="_blank" href="https://www.10printiamcool.com/custom-metric-properties-and-cloudwatch-insights">structured logging</a>, <a target="_blank" href="https://www.10printiamcool.com/adding-lambda-custom-metrics-the-easy-way">metrics</a>, and <a target="_blank" href="https://www.10printiamcool.com/creating-custom-metric-alarms-with-cdk">alarms</a>. All these play an important part in building a maintainable system. One thing that I haven't covered is distributed tracing, as I had had no experience with it. So this is a record of my first steps using <a target="_blank" href="https://aws.amazon.com/xray/">X-Ray</a>, the AWS offering you get included with your AWS account.</p>
<p>Please be aware there are numerous excellent third-party offerings, such as the following:</p>
<ul>
<li><a target="_blank" href="https://newrelic.com/">New Relic</a></li>
<li><a target="_blank" href="https://www.datadoghq.com/">Datadog</a></li>
<li><a target="_blank" href="https://lumigo.io/">Lumigo</a></li>
<li><a target="_blank" href="https://www.thundra.io/">Thundra</a></li>
</ul>
<p>In order to dip my toe into the world of X-Ray, I decided to instrument the examples I created for my <a target="_blank" href="https://www.npmjs.com/package/@andybalham/cdk-cloud-test-kit">CDK Cloud Test Kit</a> <code>npm</code> package. These example use a variety of services, e.g. SNS, SQS, EventBridge, etc, so provide a good starting point to learn.</p>
<h2 id="heading-api-gateway-lambda-and-eventbridge">API Gateway, Lambda, and EventBridge</h2>
<p>This example consists of an API Gateway backed by a Lambda function. The Lambda function does the following:</p>
<ul>
<li>Generates a unique reference</li>
<li>Stores the body of the request in S3, under the reference generated</li>
<li>Creates a pre-signed URL, allowing access to the S3 object created</li>
<li>Publishes an event to EventBridge containing the reference and the pre-signed URL</li>
</ul>
<p><img src="https://raw.githubusercontent.com/andybalham/cdk-cloud-test-kit/main/examples/request-api/images/request-api-overview.png" alt="Diagram showing the Request API construct" /></p>
<p>This gives us a nice example to see if we can trace requests from API Gateway, through a Lambda function, through EventBridge, to another Lambda function.</p>
<h3 id="heading-adding-x-ray-to-our-cdk-code">Adding X-Ray to our CDK code</h3>
<p>The <code>RequestApi</code> construct contains the <code>RestApi</code> instance and the Lambda function it calls.</p>
<p>For the <code>RestApi</code>, we set the <code>deployOptions</code> property <code>tracingEnabled</code> to be <code>true</code>:</p>
<pre><code class="lang-TypeScript"><span class="hljs-built_in">this</span>.api = <span class="hljs-keyword">new</span> RestApi(<span class="hljs-built_in">this</span>, <span class="hljs-string">'RequestApi'</span>, {
  <span class="hljs-comment">// &lt;snip&gt;</span>
  deployOptions: {
    tracingEnabled: <span class="hljs-literal">true</span>,
    <span class="hljs-comment">// &lt;snip&gt;</span>
  },
});
</code></pre>
<p>In the corresponding Lambda function, we set <code>tracing</code> to be <code>PASS_THROUGH</code>.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> eventPublisherFunction = <span class="hljs-keyword">new</span> NodejsFunction(<span class="hljs-built_in">this</span>, <span class="hljs-string">'EventPublisher'</span>, {
  <span class="hljs-comment">// &lt;snip&gt;</span>
  tracing: Tracing.PASS_THROUGH,
});
</code></pre>
<p>The options for <code>tracing</code> are:</p>
<ul>
<li><code>ACTIVE</code>: Lambda will respect any tracing header it receives from an upstream service. If no tracing header is received, Lambda will sample the request based on a fixed rate.</li>
<li><code>PASS_THROUGH</code>: Lambda will only trace the request from an upstream service if it contains a tracing header with "sampled=1"</li>
<li><code>DISABLED</code></li>
</ul>
<p>To quote the StackOverflow post <a target="_blank" href="https://stackoverflow.com/questions/64800794/what-is-active-tracing-mean-in-lambda-with-xray">What is <code>Active tracing</code> mean in lambda with Xray?</a>:</p>
<blockquote>
<p>AWS Lambda supports both active and passive instrumentation. So basically you use passive instrumentation if your function handles requests that have been sampled by some other service (e.g. API gateway). In contrast, if your function gets "raw" un-sampled requests, you should use active instrumentation, so that the sampling takes place.</p>
</blockquote>
<p>In our case, as the Lambda function is called from API Gateway, we can set the value to <code>PASS_THROUGH</code>.</p>
<p>However, in the <code>RequestApiTestStack</code> stack, we have a Lambda function that is triggered by EventBridge. Although <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-related-service-xray.html">EventBridge integrates with X-Ray</a>, it does so passively. This means that the Lambda function receiving the event needs to have its <code>tracing</code> set to <code>ACTIVE</code>.</p>
<pre><code class="lang-TypeScript"><span class="hljs-built_in">this</span>.addTestFunction(
  <span class="hljs-keyword">new</span> NodejsFunction(<span class="hljs-built_in">this</span>, RequestApiTestStack.EventObserverId, {
    <span class="hljs-comment">// &lt;snip&gt;</span>
    tracing: Tracing.ACTIVE,
  })
);
</code></pre>
<p>After deploying to AWS and running a few request, the X-Ray service map view shows the following:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/request-api-no-sdk.png?raw=true" alt="X-Ray service map showing no EventBridge" /></p>
<p>We can see API Gateway invoking the associated Lambda function, and we can also see observer Lambda function being invoked. However, clearly we are missing something that links them all together.</p>
<h3 id="heading-adding-x-ray-to-our-lambda-code">Adding X-Ray to our Lambda code</h3>
<p>To get our trace linked up, we need to wrap the <code>EventBridgeClient</code> instance with the appropriate middleware to inject the context into the calls to publish events. This is done in the <code>RequestApi.EventPublisher</code> function as follows:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> AWSXRay <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-xray-sdk'</span>;

<span class="hljs-keyword">const</span> eventBridgeClient = AWSXRay.captureAWSv3Client(<span class="hljs-keyword">new</span> EventBridgeClient({}));
</code></pre>
<p>Now when we view X-Ray, we can see the trace from API Gateway to the handle Lambda function, through EventBridge, and finally to our observer Lambda function.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/request-api-with-sdk.png?raw=true" alt="X-Ray service map showing EventBridge calls" /></p>
<p>You may notice that there are two circles in the map for each Lambda function. In the video <a target="_blank" href="https://www.youtube.com/watch?v=OOScvywKj9s">How to do Distributed tracing in AWS? | AWS X-ray and Cloudwatch Service Lens</a>, Marcia Villalba explains that there is one for the Lambda runtime and one for the handler code.</p>
<h3 id="heading-viewing-the-traces">Viewing the traces</h3>
<p>Now we have everything joined up, we can start looking at some traces to give ourself an idea of what X-Ray can do for us. Looking at the trace list, one entry jumps out as being considerably slower than the rest.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/request-api-trace-list.png?raw=true" alt="X-Ray trace list" /></p>
<p>Clicking on this, we can see the following trace. We can also see the reason for why it is slower, as what we can see is a cold start in action.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/request-api-trace-cold.png?raw=true" alt="X-Ray trace showing a cold start" /></p>
<p>If we look at a quicker trace, we can see a warmed-up Lambda function.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/request-api-trace-warm.png?raw=true" alt="X-Ray trace showing warm start" /></p>
<p>This small example gives a flavour of the insight that these traces can provide.</p>
<h2 id="heading-sns-and-sqs">SNS and SQS</h2>
<p>The <a target="_blank" href="https://github.com/andybalham/cdk-cloud-test-kit">CDK Cloud Test Kit</a> also contains a couple of examples using SNS and SQS. This gives us a chance to add X-Ray to those and see what happens.</p>
<p>For the SNS example, we wrap the <code>SNSClient</code> in the X-Ray middleware.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> sns = AWSXRay.captureAWSv3Client(<span class="hljs-keyword">new</span> SNSClient({}));
</code></pre>
<p>Running the tests, we see the following service map. This clearly shows the structure of the application, where one Lambda function publishes events to one of two topics and two Lambda functions subscribe as observers.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/event-router-relative-size.png?raw=true" alt="X-Ray service map showing relative sizes" /></p>
<p>What I also noted, was that the relative sizes indicates the weight of traffic through the system. This could be useful to see if the flow within your application is as your would expect.</p>
<p>For SQS, we again wrap the client as we have done before.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> sqs = AWSXRay.captureAWSv3Client(<span class="hljs-keyword">new</span> SQSClient({}));
</code></pre>
<p>Now when we run the tests, we see the following in X-Ray.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/message-router-with-errors.png?raw=true" alt="X-Ray service map showing metrics" /></p>
<p>The interesting thing here is that the view includes metrics along with the service map. Two of the circles indicate that error metrics were recorded. Now if we select them, we get the option to view filtered traces.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/message-router-with-error-selected.png?raw=true" alt="X-Ray service map option to view filtered traces" /></p>
<p>Clicking on this we get a list of traces where errors occurred.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/message-router-with-error-traces.png?raw=true" alt="X-Ray trace list" /></p>
<p>Selecting one, we go straight to the logs and we can see the error.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/message-router-with-error-log-entries.png?raw=true" alt="CloudWatch error logs" /></p>
<p>Hopefully, this gives you some idea of how X-Ray can help bring together traces, metrics, and logs. Allowing you to identify errors and get to the relevant logs, in order to debug issues quickly.</p>
<h2 id="heading-step-functions">Step Functions</h2>
<p>The final example that we will instrument with X-Ray is one that contains a step function. The step function implements a process that obtains a credit rating and then decides whether to accept or decline a loan. Where errors occur, a message is placed on an SQS queue. The step function definition is shown below. We can see that it includes integrations with SNS and SQS.</p>
<p><img src="https://raw.githubusercontent.com/andybalham/cdk-cloud-test-kit/main/examples/loan-processor-state-machine/images/loan-processor-step-function.png" alt="The step function definition" /></p>
<p>As well as setting the appropriate property on each Lambda function, we also need to set the <code>tracingEnabled</code> property to <code>true</code> on our step function.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> StateMachineProps {
    <span class="hljs-comment">// &lt;snip&gt;</span>
    <span class="hljs-comment">/**
     * Specifies whether Amazon X-Ray tracing is enabled for this state machine.
     *
     * @default false
     */</span>
    <span class="hljs-keyword">readonly</span> tracingEnabled?: <span class="hljs-built_in">boolean</span>;
    <span class="hljs-comment">// &lt;snip&gt;</span>
}
</code></pre>
<p>With this in place, we can deploy the updated example and run our tests that exercise all the routes through the step function. The result in X-Ray is shown below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/step-function-service-map.png?raw=true" alt="X-Ray showing step function service map" /></p>
<p>Here we can see the step function integrations with Lambda, SNS, SQS, and DynamoDB. We can see the traces go through Lambda, SNS, and SQS, but stop at DynamoDB. Although our example observes DynamoDB events, and we can see the Lambda function elsewhere in the service map, the context is lost as soon as the record is written.</p>
<p>Again, we can see the metrics overlaid on the service map. The tests include some error scenarios, and the resulting metrics are reflected in the highlighting of the services. Drilling into the step function service, we can view the error trace and we see the following.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/adventures-with-xray-part-1/step-function-error-trace.png?raw=true" alt="X-Ray showing step function error trace" /></p>
<p>Here we can clearly see the retry behaviour occurring, before the step function errors.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we looked at how we can use X-Ray to instrument a variety of services. Whilst it was simple to do, it was invasive. As in, we had to change both the CDK code and the Lambda function code. Although I haven't tried them, I believe that some of the third-party offerings avoid such changes. We also need to be aware of the cost of using X-Ray, the pricing of which can be found at <a target="_blank" href="https://aws.amazon.com/xray/pricing/">AWS X-Ray pricing</a>.</p>
<p>Although I feel I have only just scratched the surface, I can see how powerful X-Ray can be in the way it combines the service map, traces, metrics, alarms, and logs. In the second part of the series, I will take what I have found so far and try instrumenting my <a target="_blank" href="https://www.10printiamcool.com/series/enterprise-patterns-cdk">Loan Broker example application</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Encapsulation Not Always Desirable]]></title><description><![CDATA[From my earliest experience with object-oriented programming, I learnt that one of the biggest benefits of the paradigm was the concept of encapsulation. Lately, my experience with developing in JavaScript and creating libraries have led me to come t...]]></description><link>https://www.10printiamcool.com/encapsulation-not-always-desirable</link><guid isPermaLink="true">https://www.10printiamcool.com/encapsulation-not-always-desirable</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[Learning Journey]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 02 Jul 2023 16:06:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/-kr8XPYuSI8/upload/b8542d92ca1f3c28c5c8d48e969aa6bb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>From my earliest experience with <a target="_blank" href="https://en.wikipedia.org/wiki/Object-oriented_programming">object-oriented programming</a>, I learnt that one of the biggest benefits of the paradigm was the concept of encapsulation. Lately, my experience with developing in JavaScript and creating libraries have led me to come to the conclusion that it is not always desirable.</p>
<h2 id="heading-what-is-encapsulation">What is encapsulation</h2>
<p>The following links provide an admirable overview of the concept:</p>
<ul>
<li><a target="_blank" href="https://stackify.com/oop-concept-for-beginners-what-is-encapsulation/">Stackify: What is Encapsulation</a></li>
<li><a target="_blank" href="&lt;https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)#An_information-hiding_mechanism&gt;">Wikipedia: Encapsulation</a></li>
</ul>
<p>The following in particular sums up the idea:</p>
<blockquote>
<p>Under the definition that encapsulation "can be used to hide data members and member functions", the internal representation of an object is generally hidden from view outside of the object's definition. Typically, only the object's own methods can directly inspect or manipulate its fields. Hiding the internals of the object protects its integrity by preventing users from setting the internal data of the component into an invalid or inconsistent state. A supposed benefit of encapsulation is that it can reduce system complexity, and thus increase robustness, by allowing the developer to limit the interdependencies between software components</p>
</blockquote>
<h2 id="heading-encapsulation-is-the-default">Encapsulation is the default</h2>
<p>The majority of my object-oriented programming experience is in Java and C#. In both these languages, the default visibility for class members is non-public. That is, they cannot be access by any class without a special relationship with that class. Such relationships are a sub-class or a class in the same packaging unit. This implicitly pushes the developer to conceal as much as possible.</p>
<p>When I encountered TypeScript, I was surprised to find that the opposite was true. I had to explicitly make any members private. I had to consciously make the decision to hide them from potential users of the class.</p>
<h2 id="heading-when-encapsulation-gets-in-the-way">When encapsulation gets in the way</h2>
<p>This subtle change made me question my default thinking. This was coupled with my experience of using a third-party component in a way similar to the following hypothetical code:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> myClient = <span class="hljs-keyword">new</span> ServiceClient({ region: <span class="hljs-string">'eu-west-2'</span>});

<span class="hljs-comment">// ...</span>

myClient.region; <span class="hljs-comment">// Not accessible now, although it was provided earlier</span>
</code></pre>
<p>The component was hiding information that I had provided. What was the point of doing this? How was I going to misuse this information? Now I had to pass around the information that existed inside another parameter that was passed with it.</p>
<p>I also had the experience of trying to subclass a class I had published as part of an <code>npm</code> package. I had diligently hidden everything deemed 'not essential', but now had cut off the ability to extend it.</p>
<p>All this caused me to further question how I thought about member visibility.</p>
<h2 id="heading-dont-throw-the-baby-out-with-the-bathwater">Don't throw the baby out with the bathwater</h2>
<p>Should we just make everything public? Of course not. Anything public forms part of the contract of your class and you should be committed to honouring that as best you can. Once you have published, then breaking that contract could result in very unhappy clients. If you are using <a target="_blank" href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain-driven Design</a>, then you will also need to implement some business rules to keep your domain objects consistent. Again, encapsulation plays a vary valuable role here.</p>
<p>What I am thinking about here are primarily read-only properties that have been hidden without thought. Perhaps because the language made that the default.</p>
<h2 id="heading-sometimes-privacy-is-just-a-facade">Sometimes privacy is just a facade</h2>
<p>As it turns out, sometimes privacy is just a facade. Take C# for example, where you can define this seemingly well-encapsulated class.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">class</span> <span class="hljs-title">MyEncapsulatedClass</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> PrivateProperty { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">MyEncapsulatedClass</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> myPropertyValue</span>)</span>
    {
        PrivateProperty = myPropertyValue;
    }
}
</code></pre>
<p>However, using <a target="_blank" href="https://www.howtogeek.com/devops/what-is-reflection-in-programming/">reflection</a> you can still access the supposedly-private value.</p>
<pre><code class="lang-c#"><span class="hljs-keyword">var</span> myEncapsulatedInstance = <span class="hljs-keyword">new</span> MyEncapsulatedClass(<span class="hljs-number">666</span>);

<span class="hljs-keyword">var</span> myEncapsulatedClass = <span class="hljs-keyword">typeof</span>(MyEncapsulatedClass);
<span class="hljs-keyword">var</span> privateProperty =
    myEncapsulatedClass.GetProperty(
        <span class="hljs-string">"PrivateProperty"</span>, BindingFlags.NonPublic | BindingFlags.Instance);
<span class="hljs-keyword">var</span> privatePropertyValue =
    privateProperty.GetValue(myEncapsulatedInstance, <span class="hljs-literal">null</span>);

Console.WriteLine(<span class="hljs-string">"privatePropertyValue="</span> + privatePropertyValue);
</code></pre>
<p>I concede that you have to do some work here to get the value. The point I am trying to make is that you might want to check your language before relying on encapsulation for anything security-related.</p>
<h2 id="heading-summary">Summary</h2>
<p>My experience with TypeScript's public-by-default approach led me to question my default position of hiding everything possible in a class. This was combined with my experience of being frustrated with using libraries that hid information unnecessarily.</p>
<p>Now for utility libraries or packages, I try to keep things as open as possible. Where properties are passed in on construction, I try to make them public and read-only. Internal structures are made private by conscious choice, only if exposing them would make for a fragile contract.</p>
<p>When implementing <a target="_blank" href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain-driven Design</a> business rules, encapsulation becomes necessary in order to enforce those rules and keep the domain model consistent. Again, this is a conscious choice.</p>
<p>What I also learned, is that trying different programming languages can make you think differently and challenge your own assumptions and habits. This can only be a good thing.</p>
]]></content:encoded></item><item><title><![CDATA[Updating to Node.js 18 and AWS JavaScript SDK v3 - Part 2]]></title><description><![CDATA[In this series, we continue looking at upgrading a TypeScript codebase from AWS SDK V2 to AWS SDK V3. Here we concentrate on DynamoDB and also look at S3, Step Functions, and Lambda functions.
DynamoDB clients
As we saw in the first part of this seri...]]></description><link>https://www.10printiamcool.com/updating-to-nodejs-18-and-aws-javascript-sdk-v3-part-2</link><guid isPermaLink="true">https://www.10printiamcool.com/updating-to-nodejs-18-and-aws-javascript-sdk-v3-part-2</guid><category><![CDATA[AWS]]></category><category><![CDATA[CDK]]></category><category><![CDATA[lambda]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Tue, 06 Jun 2023 19:36:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/mG28olYFgHI/upload/2328dd04387a48fccd102eada6c82bc6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this series, we continue looking at upgrading a TypeScript codebase from AWS SDK V2 to AWS SDK V3. Here we concentrate on DynamoDB and also look at S3, Step Functions, and Lambda functions.</p>
<h2 id="heading-dynamodb-clients">DynamoDB clients</h2>
<p>As we saw in the <a target="_blank" href="https://www.10printiamcool.com/updating-to-nodejs-18-and-aws-javascript-sdk-v3-part-1">first part of this series</a>, converting from V2 to V3 can be as straightforward as changing the type of service client, then using that client to send a command rather than invoking a method.</p>
<p>So the following:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">readonly</span> sns: AWS.SNS;
...
<span class="hljs-keyword">const</span> publishInput: PublishInput = {
  Message: <span class="hljs-built_in">JSON</span>.stringify(message),
  TopicArn: <span class="hljs-built_in">this</span>.topicArn,
  MessageAttributes: messageAttributes,
};

<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.sns.publish(publishInput).promise();
</code></pre>
<p>Becomes:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">readonly</span> sns: SNSClient;
...
<span class="hljs-keyword">const</span> publishInput: PublishInput = {
  Message: <span class="hljs-built_in">JSON</span>.stringify(message),
  TopicArn: <span class="hljs-built_in">this</span>.topicArn,
  MessageAttributes: messageAttributes,
};

<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.sns.send(<span class="hljs-keyword">new</span> PublishCommand(publishInput));
</code></pre>
<p>The same applies for DynamoDB, there is a <code>DynamoDBClient</code> class in the <code>@aws-sdk/client-dynamodb</code> package. As expected, this can be used to send commands to put and get items. However, unlike the <code>AWS.DynamoDB.DocumentClient</code> class in the V2 SDK, the <code>DynamoDBClient</code> class only deals with objects in the low-level <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html">DynamoDB format</a>. As you can see from the example below, this is a JSON format that uses typed objects for properties:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Item"</span>: {
    <span class="hljs-attr">"Age"</span>: { <span class="hljs-attr">"N"</span>: <span class="hljs-string">"8"</span> },
    <span class="hljs-attr">"Name"</span>: { <span class="hljs-attr">"S"</span>: <span class="hljs-string">"Rover"</span> },
    <span class="hljs-attr">"Breed"</span>: { <span class="hljs-attr">"S"</span>: <span class="hljs-string">"Beagle"</span> },
    <span class="hljs-attr">"AnimalType"</span>: { <span class="hljs-attr">"S"</span>: <span class="hljs-string">"Dog"</span> }
  }
}
</code></pre>
<p>I decided to do the modern thing and ask ChatGPT if there was a way to convert the output of <code>DynamoDBClient</code>. To its credit it did point me in one of the possible directions.</p>
<blockquote>
<p>If you are using the <code>query</code> method of the <code>DynamoDBClient</code> object to query a DynamoDB table in JavaScript, you will need to unmarshall the output to convert the raw DynamoDB item format into a more usable JavaScript format.
The output of the <code>query</code> method is returned in the native DynamoDB item format, which is a complex JSON structure that includes data types and other metadata. To work with the data in a more convenient way, you will need to unmarshall the output using the <code>unmarshall</code> function provided by the <code>@aws-sdk/util-dynamodb</code> module.
Here's an example code snippet that includes unmarshalling of the query results:</p>
</blockquote>
<pre><code class="lang-javascript"><span class="hljs-comment">// Import the required modules from the AWS SDK for JavaScript V3</span>
<span class="hljs-keyword">import</span> { DynamoDBClient, QueryCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>;
<span class="hljs-keyword">import</span> { unmarshall } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/util-dynamodb"</span>;

<span class="hljs-comment">// Set the AWS region and create a new DynamoDB client object</span>
<span class="hljs-keyword">const</span> REGION = <span class="hljs-string">"us-east-1"</span>;
<span class="hljs-keyword">const</span> dynamodbClient = <span class="hljs-keyword">new</span> DynamoDBClient({ <span class="hljs-attr">region</span>: REGION });

<span class="hljs-comment">// Set the parameters for the query</span>
<span class="hljs-keyword">const</span> params = {
  <span class="hljs-attr">TableName</span>: <span class="hljs-string">"my-table"</span>,
  <span class="hljs-attr">KeyConditionExpression</span>: <span class="hljs-string">"partitionKey = :pk"</span>,
  <span class="hljs-attr">ExpressionAttributeValues</span>: {
    <span class="hljs-string">":pk"</span>: { <span class="hljs-attr">S</span>: <span class="hljs-string">"my-partition-key"</span> },
  },
};

<span class="hljs-comment">// Create a new QueryCommand object and execute the query</span>
<span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> QueryCommand(params);
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> dynamodbClient.send(command);

<span class="hljs-comment">// Unmarshall the results</span>
<span class="hljs-keyword">const</span> items = response.Items.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> unmarshall(item));

<span class="hljs-comment">// Output the results to the console</span>
<span class="hljs-built_in">console</span>.log(items);
</code></pre>
<p>Although this is correct, and the <code>unmarshall</code> function will come in useful later, this isn't the route I ended up taking. The V3 SDK uses a middleware-based approach and the <code>DynamoDBClient</code> can be wrapped with middleware to do the marshalling as part of the pipeline.</p>
<p>This approach is explained in <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html">Using the DynamoDB Document Client</a>. This shows how the <a target="_blank" href="https://www.npmjs.com/package/@aws-sdk/lib-dynamodb"><code>@aws-sdk/lib-dynamodb</code> package</a> can be used as follows.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> { DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-dynamodb'</span>;
<span class="hljs-keyword">import</span> { DynamoDBDocumentClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/lib-dynamodb"</span>;
...
<span class="hljs-comment">// Wrap a DynamoDBClient instance</span>
<span class="hljs-keyword">const</span> documentClient = DynamoDBDocumentClient.from(<span class="hljs-keyword">new</span> DynamoDBClient({ region }));
...
<span class="hljs-keyword">const</span> queryOutput = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.documentClient.send(
  <span class="hljs-keyword">new</span> QueryCommand(queryParams)
);

<span class="hljs-comment">// Return unmarshalled objects</span>
<span class="hljs-keyword">return</span> queryOutput.Items;
</code></pre>
<p>One thing to be aware of is that <code>DynamoDBDocumentClient</code> does not support all the same commands as <code>DynamoDBClient</code>. So you might need to have an instance of the latter available as well as the wrapped version.</p>
<p>In part of the codebase, a DynamoDB stream event is used to retrieve the corresponding item from the table. One thing I found was that the stream <code>AttributeValue</code> appears to no longer be compatible with DynamoDB version. To get round this, I had to add an explicit cast. It was here that the <code>unmarshall</code> function came in useful, as the key is returned in the low-level JSON format.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> { AttributeValue, DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-dynamodb'</span>;
<span class="hljs-keyword">import</span> { AttributeValue <span class="hljs-keyword">as</span> StreamAttributeValue } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda/trigger/dynamodb-stream'</span>;

<span class="hljs-keyword">async</span> getItemByEventKeyAsync&lt;T&gt;(
    eventKey: { [key: <span class="hljs-built_in">string</span>]: StreamAttributeValue } | <span class="hljs-literal">undefined</span>
  ): <span class="hljs-built_in">Promise</span>&lt;T | <span class="hljs-literal">undefined</span>&gt; {
    <span class="hljs-comment">//</span>
    <span class="hljs-keyword">if</span> (eventKey === <span class="hljs-literal">undefined</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;
    }

    <span class="hljs-comment">// Cast to prevent: 'AWSLambda.AttributeValue' is not assignable to type 'DynamoDB.AttributeValue'</span>
    <span class="hljs-keyword">const</span> key = unmarshall(eventKey <span class="hljs-keyword">as</span> Record&lt;<span class="hljs-built_in">string</span>, AttributeValue&gt;);

    <span class="hljs-keyword">return</span> getItem(<span class="hljs-built_in">this</span>.region, <span class="hljs-built_in">this</span>.tableName, key) <span class="hljs-keyword">as</span> unknown <span class="hljs-keyword">as</span> T;
  }
</code></pre>
<h2 id="heading-s3">S3</h2>
<p>One of the examples in the codebase being converted used <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html">pre-signed URLs</a> to pass data. It turns out that pre-signing has changed with the V3 SDK. There is now a separate package (<code>s3-request-presigner</code>) that you need to reference to produce a URL for a V3 command.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> { getSignedUrl } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/s3-request-presigner'</span>;
...
<span class="hljs-keyword">const</span> s3Params = {
  Bucket: bucketName,
  Key: key,
};
...
<span class="hljs-keyword">const</span> signedCommand = <span class="hljs-keyword">new</span> GetObjectCommand(s3Params);
<span class="hljs-keyword">const</span> signedUrl = <span class="hljs-keyword">await</span> getSignedUrl(s3, signedCommand, {
  expiresIn: expirySeconds ?? <span class="hljs-number">60</span>,
});
</code></pre>
<h2 id="heading-lists-now-can-return-undefined">Lists now can return <code>undefined</code></h2>
<p>Another thing that I noticed as part of the conversion process was that lists returned by the APIs can now be <code>undefined</code>. Below is an example where step function executions are being listed.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> { executions } = <span class="hljs-keyword">await</span> stepFunctions.listExecutions(opts).promise();
<span class="hljs-keyword">if</span> (executions.length &gt; <span class="hljs-number">0</span>) {
  <span class="hljs-keyword">const</span> newestRunning = executions[<span class="hljs-number">0</span>];
</code></pre>
<p>When converting, I had to add an extra test to cater for the possibility of <code>undefined</code>.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> { executions } = <span class="hljs-keyword">await</span> sfnClient.send(<span class="hljs-keyword">new</span> ListExecutionsCommand(opts)); <span class="hljs-comment">// Can be undefined</span>
<span class="hljs-keyword">if</span> (executions &amp;&amp; executions.length &gt; <span class="hljs-number">0</span>) {
  <span class="hljs-keyword">const</span> newestRunning = executions[<span class="hljs-number">0</span>];
</code></pre>
<h2 id="heading-invoking-lambda-functions">Invoking Lambda functions</h2>
<p>Another small quirk that emerged from my conversion was that I needed to encode/decode the payloads when invoking a Lambda function. The <code>Payload</code> is now returned as a <code>Uint8Array</code>, so we need to use a <code>TextEncoder</code> to convert from and to JSON objects.</p>
<p>Here we encode the stringify-ed JSON object:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> encoder = <span class="hljs-keyword">new</span> TextEncoder();
<span class="hljs-keyword">const</span> lambdaPayload = request ? { Payload: encoder.encode(<span class="hljs-built_in">JSON</span>.stringify(request)) } : {};
</code></pre>
<p>And here we decode it before parsing:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> TextDecoder();
<span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(decoder.decode(Payload));
</code></pre>
<h2 id="heading-discoverability-thoughts">Discoverability thoughts</h2>
<p>As part of the conversion process, I encountered the following code that I had in place to reuse connections in Node.js.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> documentClient = <span class="hljs-keyword">new</span> DocumentClient({
  httpOptions: {
    agent,
  },
});
</code></pre>
<p>My thought was to navigate to the definition of the new options and look for something similar. However, I quickly found myself lost.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">configuration: DynamoDBClientConfig</span>);
</code></pre>
<p>Led to...</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> DynamoDBClientConfig <span class="hljs-keyword">extends</span> DynamoDBClientConfigType {
}
</code></pre>
<p>Which led to...</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">type</span> DynamoDBClientConfigType = Partial&lt;__SmithyConfiguration&lt;__HttpHandlerOptions&gt;&gt; &amp; ClientDefaults &amp; RegionInputConfig &amp; EndpointInputConfig&lt;EndpointParameters&gt; &amp; RetryInputConfig &amp; HostHeaderInputConfig &amp; AwsAuthInputConfig &amp; UserAgentInputConfig &amp; EndpointDiscoveryInputConfig &amp; ClientInputEndpointParameters;
</code></pre>
<p>At which point I stopped and searched for 'aws sdk V3 keep-alive' and found <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-reusing-connections.html">Reusing connections with keep-alive in Node.js</a></p>
<p>This allowed me to rewrite the original as follows:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> documentClient = DynamoDBDocumentClient.from(
  <span class="hljs-keyword">new</span> DynamoDBClient({
    requestHandler: <span class="hljs-keyword">new</span> NodeHttpHandler({
      httpAgent: agent,
    }),
  })
);
</code></pre>
<p>I appreciate there is a good reason for how the options are now defined, but I do feel it has affected discoverability via the definition. I just need to remember to fall back on search and AI chatbots.</p>
<blockquote>
<p>As it turns out, this 'keep alive' code is not needed any more. See <a target="_blank" href="https://aws.amazon.com/blogs/developer/http-keep-alive-is-on-by-default-in-modular-aws-sdk-for-javascript/">HTTP keep-alive is on by default in modular AWS SDK for JavaScript</a></p>
</blockquote>
<h2 id="heading-the-middleware-based-approach">The middleware-based approach</h2>
<p>As touched on in the DynamoDB section, the V3 SDK uses a middleware-based approach. We saw it when we wrapped a <code>DynamoDBClient</code> instance in a <code>DynamoDBDocumentClient</code> instance.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> documentClient = DynamoDBDocumentClient.from(<span class="hljs-keyword">new</span> DynamoDBClient({ region }));
</code></pre>
<p>The article <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/welcome.html">What's the AWS SDK for JavaScript?</a> explains how you can create your own customisations.</p>
<blockquote>
<p>In V3, you can use a new middleware stack to control the lifecycle of an operation call. Each middleware stage in the stack calls the next middleware stage after making any changes to the request object.</p>
</blockquote>
<p>It goes on to give the following example of adding a custom header to a Amazon DynamoDB client.</p>
<pre><code class="lang-TypeScript">dbClient.middlewareStack.add(
  <span class="hljs-function">(<span class="hljs-params">next, context</span>) =&gt;</span> args =&gt; {
    args.request.headers[<span class="hljs-string">"Custom-Header"</span>] = <span class="hljs-string">"value"</span>;
    <span class="hljs-keyword">return</span> next(args);
  },
  {
    step: <span class="hljs-string">"build"</span>
  }
);

dbClient.send(<span class="hljs-keyword">new</span> PutObjectCommand(params));
</code></pre>
<p>This approach, coupled with the ability to have a smaller bundle size, helped me understand the change in approach in the V3 SDK. On the surface, the changes looked a bit like unnecessary complication.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we looked at the challenges that I had when converting code for DynamoDB, Step Functions, S3, and Lambda functions and how I solved them. Hopefully, my experience can help others. In the main, the process was quite painless. However, my codebase was small and I had integration tests to verify the changes in the cloud.</p>
<p>If you have many unit tests that mock the older SDK, then your challenges may be greater than mine. Personally, I would try to avoid mocking at that level in the first place, but that might be a subject for another post.</p>
<p>It is well worth knowing that there is much improved documentation in <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/preview/">Developer Preview</a>. This documentation is searchable and goes beyond the original auto-generated version and includes code samples.</p>
<p>For those that like to look at code, here are the links to the resulting pull requests from my upgrading:</p>
<ul>
<li><a target="_blank" href="https://github.com/andybalham/cdk-cloud-test-kit/pull/17/files#diff-7dffb5ee14c2cb1f7def89e1049402f101dd809ba41a6d02a840a5b66a2ec5fd">Cloud test kit PR</a></li>
<li><a target="_blank" href="https://github.com/andybalham/blog-enterprise-integration/pull/1/files#diff-0a8a7b936c26ea7c6aaf8da7a20ae1a1afedc69710972a49314a20339c4653c5">Loan Broker example PR</a></li>
</ul>
<h2 id="heading-links">Links</h2>
<ul>
<li><a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html">AWS SDK for JavaScript V3</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/preview/">AWS SDK for JavaScript - Developer Preview</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrating-to-v3.html">Migrating your code to SDK for JavaScript V3</a></li>
<li><a target="_blank" href="https://www.npmjs.com/package/aws-sdk-js-codemod">aws-sdk-js-codemod</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-up.html">Setting up the SDK for JavaScript</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_util_dynamodb.html">DynamoDB marshalling</a></li>
<li><a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html">Using the DynamoDB Document Client</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Updating to Node.js 18 and AWS JavaScript SDK v3 - Part 1]]></title><description><![CDATA[With the announcement of the Node.js 18.x runtime being available in AWS Lambda, AWS also changed the included version of the AWS SDK for JavaScript.

Up until Node.js 16, Lambda’s Node.js runtimes have included the AWS SDK for JavaScript version 2. ...]]></description><link>https://www.10printiamcool.com/updating-to-nodejs-18-and-aws-javascript-sdk-v3-part-1</link><guid isPermaLink="true">https://www.10printiamcool.com/updating-to-nodejs-18-and-aws-javascript-sdk-v3-part-1</guid><category><![CDATA[AWS]]></category><category><![CDATA[CDK]]></category><category><![CDATA[lambda]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Mon, 29 May 2023 17:27:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/cxoR55-bels/upload/99ad10215de8c7c91b165a049cd1826f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the announcement of the <a target="_blank" href="https://aws.amazon.com/blogs/compute/node-js-18-x-runtime-now-available-in-aws-lambda/">Node.js 18.x runtime being available in AWS Lambda</a>, AWS also changed the included version of the <a target="_blank" href="https://aws.amazon.com/sdk-for-javascript/">AWS SDK for JavaScript</a>.</p>
<blockquote>
<p>Up until Node.js 16, Lambda’s Node.js runtimes have included the AWS SDK for JavaScript version 2. This has since been superseded by the AWS SDK for JavaScript version 3, which was released in December 2020. With this release, Lambda has upgraded the version of the AWS SDK for JavaScript included with the runtime from v2 to v3.</p>
</blockquote>
<p>I also noted that when doing development, I was being nagged as follows.</p>
<pre><code class="lang-text">(node:16616) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
</code></pre>
<p>The announcement and the nag sufficiently motivated myself to look at my <a target="_blank" href="https://github.com/andybalham/cdk-cloud-test-kit">CDK Cloud Test Kit</a> and make the leap from SDK v2 to v3, whilst documenting my experience along the way.</p>
<h2 id="heading-tldr">TL;DR</h2>
<ul>
<li>Use the service clients for smaller bundle size and future-proofing</li>
<li><code>aws-sdk-js-codemod</code> works OK, but the result may be deprecated</li>
<li>CDK <code>hotswap</code> doesn't update the Node.js version</li>
</ul>
<h2 id="heading-upgrade-approach">Upgrade approach</h2>
<p>My first thought was to question how should I approach the process of upgrading. Should I uninstall the <code>aws-sdk</code> package, see what breaks, then fix it all up? Or should I take a more step-by-step approach? Ultimately, I will need to uninstall the <code>aws-sdk</code> package to be sure I have amended all references, but to keep things manageable I decided to tackle the functionality service by service.</p>
<p>When identifying what needed to change, I noted that my codebase was not consistently explicit in the Node.js version being used. The reason for this was that the code used the <code>NodejsFunction</code> <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/constructs.html">CDK construct</a> and the default value for <code>runtime</code> is <code>NODEJS_14_X</code>.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> NodejsFunctionProps <span class="hljs-keyword">extends</span> FunctionOptions {
    <span class="hljs-comment">/**
     * @default Runtime.NODEJS_14_X
     */</span>
    <span class="hljs-keyword">readonly</span> runtime?: lambda.Runtime;
}
</code></pre>
<p>With hindsight, in future I would favour being explicit with the runtime version. I think defaults have their place, but I feel such a key dependency deserves to have full visibility.</p>
<h2 id="heading-sns-first">SNS first</h2>
<p>As <a target="_blank" href="https://aws.amazon.com/codewhisperer/">CodeWhisperer</a> has just been released for personal use, I decided to give it a try with the prompt 'Publish a message to the selected output topic using the javascript sdk v3' and got the following result.</p>
<pre><code class="lang-TypeScript"><span class="hljs-comment">// Publish a message to the selected output topic using the javascript sdk v3</span>
<span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> PublishCommand({
    TopicArn: outputTopicArn,
    Message: <span class="hljs-built_in">JSON</span>.stringify(numbersEvent),
}
);

<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> sns.send(command);

<span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify({ result }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));
</code></pre>
<p>However, when I ran my cloud-based tests, I got the failure below:</p>
<pre><code class="lang-text">  SimpleEventRouter Test Suite
spec.js:54
    1) Routes positive sums
    &lt;snip&gt;
  0 passing (1m)
base.js:379
  5 failing
</code></pre>
<p>Looking in CloudWatch, I could see that the <code>@aws-sdk/client-sns</code> package could not be found.</p>
<pre><code class="lang-text">2023-05-07T07:23:27.329Z  undefined ERROR Uncaught Exception
{
    "errorType": "Runtime.ImportModuleError",
    "errorMessage": "Error: Cannot find module '@aws-sdk/client-sns'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/Runtime.js\n- /var/runtime/index.js",
    "stack": [
        "Runtime.ImportModuleError: Error: Cannot find module '@aws-sdk/client-sns'",
        "Require stack:",
        &lt;snip&gt;
    ]
}
</code></pre>
<p>The reason for this turned out to be that I was using the <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/cli.html"><code>--hotswap</code></a> option with <code>cdk deploy</code>. This updates the Lambda function code, but not the runtime. As <code>@aws-sdk/client-sns</code> is not bundled with the Node.js 14 runtime, we get the error above.</p>
<p>When a full deployment was done, we got the happy sight of all the tests passing. As these are cloud-based, I have a high-confidence in a successful migration.</p>
<pre><code class="lang-text">  SimpleEventRouter Test Suite
spec.js:54
    √ Routes positive sums (4175ms)
    &lt;snip&gt;
  5 passing (13s)
</code></pre>
<h2 id="heading-sqs-next-with-aws-sdk-js-codemod">SQS next with <code>aws-sdk-js-codemod</code></h2>
<p>The <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrating-to-v3.html">AWS documentation</a> on upgrading mentions a package called <a target="_blank" href="https://www.npmjs.com/package/aws-sdk-js-codemod">aws-sdk-js-codemod</a>. To quote the README, 'This repository contains a collection of codemod scripts for use with JSCodeshift that help update AWS SDK for JavaScript APIs.' This sounded promising, so I decided to give it a go.</p>
<p>I followed the instructions and ran the following, pointing at TypeScript file with SQS references.</p>
<pre><code class="lang-text">npx aws-sdk-js-codemod -t v2-to-v3 PATH...
</code></pre>
<p>The results can be seen below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/upgrade-to-sdk-v3/codemod-sqs-upgrade-1.png?raw=true" alt="codemod import updates" /></p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/upgrade-to-sdk-v3/codemod-sqs-upgrade-2.png?raw=true" alt="codemode code updates" /></p>
<p>This all look reasonable. I needed to install the <code>@aws-sdk/client-sqs</code> package and to add an empty configuration to the <code>SQS</code> constructor (<code>new SQS({});</code>), but after that I was able to deploy and test.</p>
<pre><code class="lang-text">  SimpleMessageRouter Test Suite
spec.js:54
    √ Routes as expected: {"values":[],"isExpectedPositive":true} (4178ms)
    &lt;snip&gt;
  6 passing (28s)
</code></pre>
<p>So success, but the style looks a little different from the SNS.</p>
<h2 id="heading-why-does-codemod-sqs-code-differ-from-the-sns-code">Why does <code>codemod</code> SQS code differ from the SNS code?</h2>
<p>I decided to ask CodeWhisperer how to send an SQS message using the v3 SDK and got another way.</p>
<pre><code class="lang-TypeScript"><span class="hljs-comment">// Send an SQS message using v3 sdk</span>
<span class="hljs-keyword">const</span> sendMessageRequest: AWS_SQS.SendMessageRequest = {
    QueueUrl: outputQueueUrl,
    MessageBody: <span class="hljs-built_in">JSON</span>.stringify(numbersMessage),
};
<span class="hljs-keyword">await</span> sqs.sendMessage(sendMessageRequest);
</code></pre>
<p>I was getting, if not confused, a little intrigued by these alternatives. Reading the
<a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/preview/client/sqs/command/SendMessageCommand/">AWS documentation</a>, I could see that it points you down the <code>SQSClient</code> route as shown below.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { SQSClient, SendMessageCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sqs"</span>; <span class="hljs-comment">// ES Modules import</span>
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> SQSClient(config);
<span class="hljs-keyword">const</span> input = {
  <span class="hljs-comment">// SendMessageRequest</span>
  <span class="hljs-attr">QueueUrl</span>: <span class="hljs-string">"STRING_VALUE"</span>, <span class="hljs-comment">// required</span>
  <span class="hljs-attr">MessageBody</span>: <span class="hljs-string">"STRING_VALUE"</span>, <span class="hljs-comment">// required</span>
};
<span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> SendMessageCommand(input);
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(command);
</code></pre>
<p>I tried this approach and, unsurprisingly, this worked as well. So now, we have three possible ways:</p>
<ul>
<li>Use <code>SQS.sendMessage()</code><ul>
<li>With <code>SendMessageCommandInput</code></li>
<li>With <code>SendMessageRequest</code></li>
</ul>
</li>
<li>Use <code>SQSClient.send()</code> with <code>SendMessageCommand</code></li>
</ul>
<blockquote>
<p><code>SendMessageCommandInput</code> turns out to be a subclass of <code>SendMessageRequest</code> as documented <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-sqs/modules/sendmessagerequest.html">here</a>.</p>
</blockquote>
<p>So, which to use?</p>
<p>I dug a little further into the documentation and found this on the <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-sqs/#v2-compatible-style">'v2 compatible style'</a> (highlighting my own).</p>
<blockquote>
<p>The client can also send requests using v2 compatible style. However, it results in a bigger bundle size and <strong>may be dropped in next major version</strong>. More details in the blog post on <a target="_blank" href="https://aws.amazon.com/blogs/developer/modular-packages-in-aws-sdk-for-javascript/">modular packages in AWS SDK for JavaScript</a></p>
</blockquote>
<p>The key takeaway for me here is that if you take the easiest approach now with your codebase, then you may face another round of updates if your application sees out the support lifetime of SDK v3. I noted that this is the approach that the <code>aws-sdk-js-codemod</code> defaults to, so that is something to bear in mind.</p>
<h2 id="heading-summary">Summary</h2>
<p>So far, the process of updating has been pretty painless. Admittedly, I have been tackling a small codebase and only two AWS services. On a larger codebase that has not wrapped the AWS services in more domain-level abstractions, then this could be quite a task. Especially if it is not easy to exercise the code thoroughly in the cloud.</p>
<p>In the next post, I will move on to updating the rest of the AWS services being used, including DynamoDB and the marshalling challenge.</p>
]]></content:encoded></item><item><title><![CDATA[Creating Overloaded Methods in TypeScript]]></title><description><![CDATA[For the majority of my journey in software development, I have used languages that have supported method overloading. I have found this to be useful, so I have been wondering how to implement something similar in TypeScript. How can we have a method ...]]></description><link>https://www.10printiamcool.com/creating-overloaded-methods-in-typescript</link><guid isPermaLink="true">https://www.10printiamcool.com/creating-overloaded-methods-in-typescript</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Programming Tips]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Sun, 16 Apr 2023 08:13:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Oxl_KBNqxGA/upload/4231fa3b0caf9d8851baf98d53002ece.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the majority of my journey in software development, I have used languages that have supported method overloading. I have found this to be useful, so I have been wondering how to implement something similar in <a target="_blank" href="https://www.typescriptlang.org/">TypeScript</a>. How can we have a method can be declared multiple times with different parameters in a language which does not support method overloading in the traditional way?</p>
<blockquote>
<p>After posting, it was brought to my attention that there is another way. I have added an update at the end to cover this approach.</p>
</blockquote>
<h2 id="heading-the-problem">The Problem</h2>
<p>I wanted to create a class to build queries for <a target="_blank" href="https://aws.amazon.com/dynamodb/">DynamoDB</a>. DynamoDB is a NoSQL database that indexes each item by two keys. A partition key and a sort key. When querying a DynamoDB table you always supply a partition key, and you optionally supply a sort key along with an operator such as 'greater than'. Another option is to supply two sort key values to provide a range.</p>
<h2 id="heading-the-c-solution">The C# Solution</h2>
<p>In <a target="_blank" href="https://dotnet.microsoft.com/en-us/languages/csharp">C#</a>, we could define as follows:</p>
<pre><code class="lang-c#"><span class="hljs-keyword">enum</span> SortKeyOperator
{
    EQUALS,
    LESS_THAN,
    LESS_THAN_OR_EQUAL,
    GREATER_THAN_OR_EQUAL,
    GREATER_THAN,
    BEGINS_WITH,
}

<span class="hljs-keyword">class</span> <span class="hljs-title">QueryBuilder</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Build</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> partitionKeyValue</span>)</span>
    {
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Build</span>(<span class="hljs-params">
        <span class="hljs-keyword">string</span> partitionKeyValue,
        <span class="hljs-keyword">string</span> sortKeyValue</span>)</span>
    {
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Build</span>(<span class="hljs-params">
        <span class="hljs-keyword">string</span> partitionKeyValue,
        SortKeyOperator sortKeyOperator,
        <span class="hljs-keyword">string</span> sortKeyValue</span>)</span>
    {
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Build</span>(<span class="hljs-params">
        <span class="hljs-keyword">string</span> partitionKeyValue,
        <span class="hljs-keyword">string</span> sortKeyFromValue,
        <span class="hljs-keyword">string</span> sortKeyToValue</span>)</span>
    {
    }
}
</code></pre>
<p>This is allowed, as the combination of parameters means that each method signature is unique, even though the name of the method is not. When using the Visual Studio <a target="_blank" href="https://www.freecodecamp.org/news/what-is-an-ide-in-programming-an-ide-definition-for-developers/">IDE</a> the intellisense prompts as follows.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/csharp-basic-overload-dropdown-1.png?raw=true" alt="An example of how overloaded methods appear in Visual Studio" /></p>
<p>This allows the user to scroll through the various overloaded versions of the method.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/csharp-basic-overload-dropdown-2.png?raw=true" alt="An example of how overloaded methods appear in Visual Studio" /></p>
<p>By adding documentation to the methods, you can clearly communication the intended use of each overload.</p>
<h2 id="heading-typescript-attempt-no1-separate-methods">TypeScript Attempt No.1 - Separate Methods</h2>
<p>The simplest way I could think of to try to replicate method overloading is to have separate methods that share a common prefix. In this case, <code>buildWith</code>. The result is as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> QueryBuilder {
  buildWithPartitionKeyOnly(partitionKeyValue: <span class="hljs-built_in">string</span>) {...}

  buildWithSortKey(partitionKeyValue: <span class="hljs-built_in">string</span>, sortKeyValue: <span class="hljs-built_in">string</span>) {...}

  buildWithComparison(
    partitionKeyValue: <span class="hljs-built_in">string</span>,
    sortKeyOperator: SortKeyOperator,
    sortKeyValue: <span class="hljs-built_in">string</span>
  ) {...}

  buildWithRange(
    partitionKeyValue: <span class="hljs-built_in">string</span>,
    sortKeyFromValue: <span class="hljs-built_in">string</span>,
    sortKeyToValue: <span class="hljs-built_in">string</span>
  ) {...}
}
</code></pre>
<p>This would result in the following prompt when using VS Code:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/typescript-basic-overload-1.png?raw=true" alt="An example of how a named method appears in VS Code" /></p>
<p>I actually think this approach has some merit. The explicit naming provides some level of self-documentation. A downside is that the underlying implementation might need either some duplication in the separate methods, or some common code outside them.</p>
<h2 id="heading-typescript-attempt-no2-optional-parameters">TypeScript Attempt No.2 - Optional Parameters</h2>
<p>Another approach is to use optional parameters and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">deconstructed parameters</a>. We can define a single method with a single object parameter, and we can make the sort key parameters all optional. The result is as follows:</p>
<pre><code class="lang-typescript">build({
  partitionKeyValue,
  sortKeyValue,
  sortKeyComparison,
  sortKeyRange,
}: {
  partitionKeyValue: <span class="hljs-built_in">string</span>;
  sortKeyValue?: <span class="hljs-built_in">string</span>;
  sortKeyComparison?: {
    operator: SortKeyOperator;
    value: <span class="hljs-built_in">string</span>;
  };
  sortKeyRange?: {
    fromValue: <span class="hljs-built_in">string</span>;
    toValue: <span class="hljs-built_in">string</span>;
  };
}) {
  <span class="hljs-keyword">if</span> (sortKeyValue) {
    <span class="hljs-comment">// Handle case where we match by value equality</span>
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (sortKeyComparison) {
    <span class="hljs-comment">// Handle case where we match by comparison</span>
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (sortKeyRange) {
    <span class="hljs-comment">// Handle case where we match by range</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Handle case where we match just by primary key</span>
  }
}
</code></pre>
<p>Whilst this works, it isn't obvious to the caller what combination of parameters should be used to get the various outcomes. For example, can <code>sortKeyRange</code> be used with <code>sortKeyValue</code>? The only way to know this, is to look inside the method. Not ideal. Can we do better?</p>
<h2 id="heading-typescript-attempt-no3-naive-discriminated-types">TypeScript Attempt No.3 - Naive Discriminated Types</h2>
<p>TypeScript allows you to define that a value can be one of a set of types, for example:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">var</span> v: <span class="hljs-built_in">number</span> | <span class="hljs-built_in">string</span>;
</code></pre>
<p>Can we take advantage of this to give the callers of the method a set of exclusive choices, so that they do not need to look inside the method to work out how to use it?</p>
<p>Below was my first effort:</p>
<pre><code class="lang-typescript">build({
  partitionKeyValue,
  sortKeyCriteria,
}: {
  partitionKeyValue: <span class="hljs-built_in">string</span>;
  sortKeyCriteria?:
    | {
        value: <span class="hljs-built_in">string</span>;
      }
    | {
        comparison: {
          operator: SortKeyOperator;
          value: <span class="hljs-built_in">string</span>;
        };
      }
    | {
        range: {
          fromValue: <span class="hljs-built_in">string</span>;
          toValue: <span class="hljs-built_in">string</span>;
        };
      };
}) {
  <span class="hljs-keyword">if</span> (sortKeyCriteria) {
    <span class="hljs-keyword">if</span> (<span class="hljs-string">'value'</span> <span class="hljs-keyword">in</span> sortKeyCriteria) {
      <span class="hljs-comment">// Handle case where we match by value equality</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-string">'comparison'</span> <span class="hljs-keyword">in</span> sortKeyCriteria) {
      <span class="hljs-comment">// Handle case where we match by comparison</span>
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-string">'range'</span> <span class="hljs-keyword">in</span> sortKeyCriteria) {
      <span class="hljs-comment">// Handle case where we match by range</span>
    } <span class="hljs-keyword">else</span> {
    }
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Handle case where we match just by primary key</span>
  }
}
</code></pre>
<p>Here we use the <code>in</code> operator to work out which of the types has been specified. This all seemed to be working as I expected until I tried the following:</p>
<pre><code class="lang-typescript">queryBuilder.build({
  partitionKeyValue: <span class="hljs-string">"pk"</span>,
  sortKeyCriteria: {
    value: <span class="hljs-string">"sortKeyValue"</span>,
    range: {
      fromValue: <span class="hljs-string">"sortKeyValue1"</span>,
      toValue: <span class="hljs-string">"sortKeyValue2"</span>,
    },
    comparison: {
      operator: SortKeyOperator.GREATER_THAN,
      value: <span class="hljs-string">"sortKeyValue"</span>,
    },
  },
});
</code></pre>
<p>I was expecting a compiler error, as I had specified all three options. However, clearly TypeScript does not work that way. What I had I done wrong?</p>
<h2 id="heading-typescript-attempt-no4-discriminated-types-done-properly">TypeScript Attempt No.4 - Discriminated Types Done Properly</h2>
<p>The solution came from an <a target="_blank" href="https://www.typescriptlang.org/play#example/discriminate-types">example in the TypeScript playground</a>. What I needed to do was define a value that would discriminate the types. The result is as follows:</p>
<pre><code class="lang-typescript">build({
  partitionKeyValue,
  sortKeyCriteria,
}: {
  partitionKeyValue: <span class="hljs-built_in">string</span>;
  sortKeyCriteria?:
    | {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'value'</span>;
        value: <span class="hljs-built_in">string</span>;
      }
    | {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'comparison'</span>;
        operator: SortKeyOperator;
        value: <span class="hljs-built_in">string</span>;
      }
    | {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'range'</span>;
        fromValue: <span class="hljs-built_in">string</span>;
        toValue: <span class="hljs-built_in">string</span>;
      };
}) {
  <span class="hljs-keyword">if</span> (sortKeyCriteria?.type === <span class="hljs-string">'value'</span>) {
    <span class="hljs-comment">// Handle case where we match by value equality</span>
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (sortKeyCriteria?.type === <span class="hljs-string">'comparison'</span>) {
    <span class="hljs-comment">// Handle case where we match by comparison</span>
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (sortKeyCriteria?.type === <span class="hljs-string">'range'</span>) {
    <span class="hljs-comment">// Handle case where we match by range</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Handle case where we match just by primary key</span>
  }
}
</code></pre>
<p>Now when using the class in VS Code, when you select the <code>type</code> you get the corresponding options. For example, when using <code>range</code> you get prompted for the relevant <code>fromValue</code> and <code>toValue</code> values.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/typescript-discriminated-type-prompt.png?raw=true" alt="An example showing how using the type filters the options shown" /></p>
<p>Now we have a single method that presents a set of mutually exclusive choices. The caller cannot get the parameters wrong and doesn't need to look inside the method.</p>
<h2 id="heading-documentation">Documentation</h2>
<p>One downside to using deconstructed parameters is that I could not find a way to document them well using <a target="_blank" href="https://en.wikipedia.org/wiki/JSDoc">JSDoc</a>. The best I could come up with was the following.</p>
<pre><code class="lang-TypeScript"><span class="hljs-comment">/**
 * Builds a query based on the key criteria supplied.
 * @param param0 Key criteria
 */</span>
build({ partitionKeyValue, sortKeyCriteria }: {...}) {...}
</code></pre>
<p>This resulted in the following prompt in VS Code, which does give some indication of the options via the <code>type</code> values.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/typescript-deconstructed-jsdoc.png?raw=true" alt="An example showing basic documentation of the method with deconstructed parameters" /></p>
<p>I may have been missing something, but for this reason I found myself quite liking the solution with separate methods. That approach was easy to document and also somewhat documented itself with the verbose names.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/typescript-overload-1.png?raw=true" alt="An example of how a named method appears in VS Code" /></p>
<p>Here we can see that the individual parameters can be documented in the same way that they can be in the C# example, as shown below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/csharp-overload-dropdown-2.png?raw=true" alt="An example of how overloaded methods appear in Visual Studio" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this post we looked at various ways that we can implement some form of method overloading in TypeScript and compared these with an equivalent in C#. My feeling is that for internal libraries, I would favour the discriminated type approach. However, for external libraries, I feel that the ability to fully document means that the simple, multi-named approach would be better. Behind the scenes, these methods may map onto a single discriminated type method, in order to keep functionality together.</p>
<h2 id="heading-update">Update</h2>
<p>After posting this, it was pointed out to me that there is another way that we can implement method overloading in TypeScript.</p>
<p>The way we can do it is by defining an <code>args</code> parameter with the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">JavaScript spread syntax</a> and a list of possible parameters. In our example this would be the following:</p>
<pre><code class="lang-TypeScript">build(
  ...args:
    | [partitionKeyValue: <span class="hljs-built_in">string</span>]
    | [partitionKeyValue: <span class="hljs-built_in">string</span>, sortKeyValue: <span class="hljs-built_in">string</span>]
    | [
        partitionKeyValue: <span class="hljs-built_in">string</span>,
        sortKeyOperator: NumericSortKeyOperator,
        sortKeyValue: <span class="hljs-built_in">string</span>
      ]
    | [
        partitionKeyValue: <span class="hljs-built_in">string</span>,
        sortKeyFromValue: <span class="hljs-built_in">string</span>,
        sortKeyToValue: <span class="hljs-built_in">string</span>
      ]
) {...}
</code></pre>
<p>In VS Code, this gives an experience very similar to that we had for C#:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/arg-list-overload-dropdown.png?raw=true" alt="VS Code showing the list of overloaded methods" /></p>
<p>We still have the problem of how to know which overload is being called. I solved this by building up a signature string from the arguments types and switching on the result.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> signature = args
  .map(<span class="hljs-function">(<span class="hljs-params">arg</span>) =&gt;</span> <span class="hljs-keyword">typeof</span> arg)
  .reduce(<span class="hljs-function">(<span class="hljs-params">accumulator, argType</span>) =&gt;</span> <span class="hljs-string">`<span class="hljs-subst">${accumulator}</span><span class="hljs-subst">${argType}</span>|`</span>, <span class="hljs-string">'|'</span>);

<span class="hljs-keyword">switch</span> (signature) {
  <span class="hljs-keyword">case</span> <span class="hljs-string">'|string|'</span>:
    <span class="hljs-comment">// Handle case where we match by partition key only</span>
    <span class="hljs-keyword">break</span>;
  <span class="hljs-keyword">case</span> <span class="hljs-string">'|string|string|'</span>:
    <span class="hljs-comment">// Handle case where we match by compound key</span>
    <span class="hljs-keyword">break</span>;
  <span class="hljs-keyword">case</span> <span class="hljs-string">'|string|string|string|'</span>:
    <span class="hljs-comment">// Handle case where we match by range</span>
    <span class="hljs-keyword">break</span>;
  <span class="hljs-keyword">case</span> <span class="hljs-string">'|string|number|string|'</span>:
    <span class="hljs-comment">// Handle case where we match by comparison</span>
    <span class="hljs-keyword">break</span>;
  <span class="hljs-keyword">default</span>:
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Unhandled signature`</span>);
}
</code></pre>
<p>One thing I did have to change was the type of <code>enum</code>. Originally, it was a set of strings, but this would cause a clash of signatures. I changed it for a set of integers and this avoided the issue.</p>
<p>I used array destructuring to access the values as follows:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">case</span> <span class="hljs-string">'|string|number|string|'</span>:
  <span class="hljs-comment">// Handle case where we match by comparison</span>
  {
    <span class="hljs-keyword">const</span> [partitionKeyValue, sortKeyOperator, sortKeyValue] = args;
    <span class="hljs-comment">// Call the method implementation</span>
  }
  <span class="hljs-keyword">break</span>;
</code></pre>
<p>Whilst TypeScript does infer the types, it does not discriminate. So it can only assert that some values are one of a set:</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/creating-overloaded-methods-in-typescript/arg-list-inferred-typing.png?raw=true" alt="VS Code showing the inferred types" /></p>
<p>This approach suffers from the documentation issue that other advanced approaches do. My feeling overall is that, although it gives a similar intellisense experience, it falls down when implementing the underlying functionality and I would still be tempted to go down the explicit naming route with discriminated types underneath.</p>
]]></content:encoded></item><item><title><![CDATA[Enterprise Integration Patterns - Domain Observability]]></title><description><![CDATA[In the first two parts in this series, we first looked at choosing a messaging technology and then looked at how we can design the domain events that flow through the application. In this part, we will look at how we can use those domain events to im...]]></description><link>https://www.10printiamcool.com/enterprise-integration-patterns-domain-observability</link><guid isPermaLink="true">https://www.10printiamcool.com/enterprise-integration-patterns-domain-observability</guid><category><![CDATA[AWS]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[Andy Blackledge]]></dc:creator><pubDate>Wed, 29 Mar 2023 19:55:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZuXDaoIx_Bc/upload/cc4d2d01b0c295c83f9b10462ac7c509.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the first two parts in this series, we first looked at <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-with-serverless-and-cdk">choosing a messaging technology</a> and then looked at how we can <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-domain-event-design">design the domain events</a> that flow through the application. In this part, we will look at how we can use those domain events to implement an observability stack. This stack will output a range of business metrics that can be used to provide visibility of the system performance and to alert us when this is not as desired.</p>
<p>The application in question acts as a loan broker, it receives a request containing the details of the loan required via an API, and then returns the best rate to a <a target="_blank" href="https://www.getvero.com/resources/webhooks/">webhook</a>.</p>
<p>The following diagram shows how we use a central <a target="_blank" href="https://aws.amazon.com/eventbridge/">EventBridge</a> event bus to implement this application using an event-driven design.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/case-study-eventbridge.png?raw=true" alt="Architecture diagram using EventBridge" /></p>
<p>The code for this post can be found in the accompanying <a target="_blank" href="https://github.com/andybalham/blog-enterprise-integration/tree/blog-part-3">GitHub repo</a>.</p>
<h2 id="heading-business-metrics-vs-system-metrics">Business metrics vs. System metrics</h2>
<p>By default, AWS outputs a large number of metrics that you can use to visualise and monitor the health of your application. For example, the <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics.html">Working with Lambda function metrics</a> AWS documentation page goes into the detail of what is outputted by default by Lambda functions. You get invocation metrics, such as the number of times that your function code is invoked, performance metrics, such as the amount of time that your function code spends processing an event, and concurrency metrics, such as the number of function instances that are processing events.</p>
<p>These provide an invaluable insight into the health of your application, but don't necessarily answer business-level questions such as, "How many quotes are we receiving per hour?", "How fast are we processing them?", or "How many rates are we receiving from lender X?". Whilst AWS provides system metrics, to answer these business-level questions we need business metrics.</p>
<p>This is where we can take advantage of our event-driven architecture. The application is already producing events such as the following:</p>
<ul>
<li><code>QuoteSubmitted</code></li>
<li><code>CreditReportFailed</code></li>
<li><code>LenderRateReceived</code></li>
</ul>
<p>What we can do is subscribe to these events and translate them into custom business metrics. We can then build dashboards, alarms, and whatever else we want on top of those metrics.</p>
<h2 id="heading-decoupling-observability">Decoupling observability</h2>
<p>In the first part of the <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-with-serverless-and-cdk">series</a>, we considered using <a target="_blank" href="https://aws.amazon.com/sqs/">SQS</a> as part of our messaging technology. One limitation of SQS is that each message can only be processed by one consumer. Our use of EventBridge has the advantage that we can subscribe to any business events without affecting any existing processing. This means we can add an observability stack entirely independently of the existing application. This demonstrates the high-level of decoupling that can be achieved and the extensibility you get with an event-driven architecture.</p>
<p>The diagram below shows how we are going add business observability to our architecture. As you can see, it simply plugs into the event bus.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/case-study-observability.png?raw=true" alt="Architecture diagram with observability added" /></p>
<h2 id="heading-simple-logging-and-logs-insights">Simple logging and Logs Insights</h2>
<p>One simple way to turn the business events into a searchable resource is to log our business events in a <a target="_blank" href="https://stackify.com/what-is-structured-logging-and-why-developers-need-it/">structured way</a>. This can be done via the following single-line Lambda function.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: EventBridgeEvent&lt;<span class="hljs-string">'DomainEventBase'</span>, DomainEventBase&gt;
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
  <span class="hljs-built_in">console</span>.log({ ...event.detail.metadata, data: event.detail.data });
};
</code></pre>
<p>This will result in log entries such as the following.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"QuoteSubmitted"</span>,
  <span class="hljs-attr">"eventVersion"</span>: <span class="hljs-string">"1.0"</span>,
  <span class="hljs-attr">"correlationId"</span>: <span class="hljs-string">"ac702216-ac35-48cb-be3c-26beb523897e"</span>,
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"35fb604e-c56b-457b-97a8-fbdae9fd0644"</span>,
  <span class="hljs-attr">"eventId"</span>: <span class="hljs-string">"8e71aecb-839f-4477-a9db-5353e1f23d04"</span>,
  <span class="hljs-attr">"domain"</span>: <span class="hljs-string">"LoanBroker"</span>,
  <span class="hljs-attr">"service"</span>: <span class="hljs-string">"RequestApi"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2023-01-28T15:05:16.237Z"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"quoteReference"</span>: <span class="hljs-string">"2023-01-28-RHJ9YUL71"</span>,
    <span class="hljs-attr">"quoteRequestDataUrl"</span>: <span class="hljs-string">"https://requestapistack-requestapibucket...-Amz-SignedHeaders=host"</span>
  }
}
</code></pre>
<p>We have flattened the metadata about the event, such as the correlation id and event type, and also included the actual data for the event, such as the quote reference. In the second part of the <a target="_blank" href="https://www.10printiamcool.com/enterprise-integration-patterns-domain-event-design">series</a>, we designed our domain events to be self-contained and to be made up from metadata about the event and the event data itself. We are taking advantage of the consistency here to output an easily searchable entry.</p>
<p>We also made another choice when designing out domain events. That was to pass large or sensitive data as time-limited <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html">presigned URLs</a>. This choice means that we are free to log the events without the risk of logging sensitive information by accident.</p>
<p>Now we have our structured logging Lambda function, we can look at how we hook it up to monitor our application. To do this we create a <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/constructs.html">CDK construct</a> as shown below.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ObserverProps {
  loanBrokerEventBus: EventBus;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> Observer <span class="hljs-keyword">extends</span> Construct {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">scope: Construct, id: <span class="hljs-built_in">string</span>, props: ObserverProps</span>) {
    <span class="hljs-built_in">super</span>(scope, id);

    <span class="hljs-keyword">const</span> loggerFunction = <span class="hljs-keyword">new</span> NodejsFunction(<span class="hljs-built_in">this</span>, <span class="hljs-string">'Logger'</span>);

    <span class="hljs-keyword">const</span> domainEventRule = <span class="hljs-keyword">new</span> Rule(<span class="hljs-built_in">this</span>, id, {
      eventBus: props.loanBrokerEventBus,
      eventPattern: {
        detail: {
          metadata: {
            domain: [EventDomain.LoanBroker],
          },
        },
      },
    });

    domainEventRule.addTarget(<span class="hljs-keyword">new</span> LambdaFunction(loggerFunction));
}
</code></pre>
<p>The construct properties allow us to pass in the event bus to subscribe to. We create our Lambda function, along with a rule that will listen to all domain events in the application. Finally, we add our Lambda function as the target for the rule.</p>
<p>The final step is to define a stack and add it to the application.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ObservabilityStackProps <span class="hljs-keyword">extends</span> StackProps {
  loanBrokerEventBus: EventBus;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> ObservabilityStack <span class="hljs-keyword">extends</span> Stack {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">scope: Construct, id: <span class="hljs-built_in">string</span>, props: ObservabilityStackProps</span>) {
    <span class="hljs-built_in">super</span>(scope, id, props);

    <span class="hljs-keyword">new</span> Observer(<span class="hljs-built_in">this</span>, <span class="hljs-string">'Observer'</span>, {
      loanBrokerEventBus: props.loanBrokerEventBus,
    });
  }
}

<span class="hljs-keyword">new</span> ObservabilityStack(app, <span class="hljs-string">'ObservabilityStack'</span>, {
  loanBrokerEventBus: messagingStack.loanBrokerEventBus,
});
</code></pre>
<p>With this Lambda function in place, we can now use CloudWatch logs to see all the domain events in a single place.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/cloudwatch-event-log-unfiltered.png?raw=true" alt="Unfiltered CloudWatch log showing business events" /></p>
<p>We can take advantage of <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#matching-terms-events">log filtering</a> to provide a more focussed view of the events.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/cloudwatch-event-log-filtered.png?raw=true" alt="Filtered CloudWatch log showing just business events" /></p>
<p>However, we can do even better by using <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html">CloudWatch Logs Insights</a>. To quote the article:</p>
<blockquote>
<p>CloudWatch Logs Insights enables you to interactively search and analyze your log data in Amazon CloudWatch Logs. You can perform queries to help you more efficiently and effectively respond to operational issues. If an issue occurs, you can use CloudWatch Logs Insights to identify potential causes and validate deployed fixes.</p>
</blockquote>
<p>This allows us to run a query such as the following and get a picture of the domain events flowing through the system.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/log-insights-query.png?raw=true" alt="A basic Logs Insights query" /></p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/log-insights-results.png?raw=true" alt="Results from a basic Logs Insights query" /></p>
<p>As we log correlation ids, we can use these when we want to focus in on a particular request by adding criteria to our queries. The query below show how this is done.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/log-insights-query-with-request-id.png?raw=true" alt="Logs Insights query for a specific request id" /></p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/log-insights-results-for-request-id.png?raw=true" alt="Results from a Logs Insights query for a specific request id" /></p>
<h2 id="heading-logging-business-metrics">Logging business metrics</h2>
<p>As well as outputting structured logs, we can also use this subscription to publish custom <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/working_with_metrics.html">Amazon CloudWatch metrics</a>. By <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html">publishing custom metrics</a> we can create dashboards and <a target="_blank" href="https://docs.aws.amazon.com/managedservices/latest/userguide/custom-cloudwatch-events.html">alarms</a>.</p>
<p>To make our lives easier, we are going to use the <a target="_blank" href="https://awslabs.github.io/aws-lambda-powertools-typescript/latest/">AWS Lambda Powertools for TypeScript</a> Metrics npm package. This will allow us to create custom metrics asynchronously by logging metrics to standard output following <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html">Amazon CloudWatch Embedded Metric Format (EMF)</a>.</p>
<p>The metric we are going to capture is a count of how many times the call to the credit report service has failed.</p>
<p>We start by declaring a <code>Metrics</code> instance outside of the Lambda function handler. We assign it a namespace and service name using exported constants, which we will use later for setting up an alarm.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> { Metrics, MetricUnits } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-lambda-powertools/metrics'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> METRICS_NAMESPACE = <span class="hljs-string">'LoanBroker'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> METRICS_SERVICE_NAME = <span class="hljs-string">'observer'</span>;

<span class="hljs-keyword">const</span> metrics = <span class="hljs-keyword">new</span> Metrics({
  <span class="hljs-keyword">namespace</span>: METRICS_NAMESPACE,
  serviceName: METRICS_SERVICE_NAME,
});
</code></pre>
<p>The next step is to create the handler to publish the metric. First, we create a function to publish a count of all the failure events. Along with adding one to the overall count, we also publish metadata about the metric. This includes as much contextual information as possible, so that the resulting log entry can aid us in debugging what failed.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> CREDIT_REPORT_FAILED_METRIC = <span class="hljs-string">'creditReportFailed'</span>;

<span class="hljs-keyword">const</span> publishCreditReportFailedMetrics = (
  creditReportFailed: CreditReportFailedV1
): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
  metrics.addMetric(CREDIT_REPORT_FAILED_METRIC, MetricUnits.Count, <span class="hljs-number">1</span>);

  addMetadata(creditReportFailed, {
    quoteReference: creditReportFailed.data.quoteReference,
    error: creditReportFailed.data.error,
    executionId: creditReportFailed.data.executionId,
    executionStartTime: creditReportFailed.data.executionStartTime,
    stateMachineId: creditReportFailed.data.stateMachineId,
  });

  metrics.publishStoredMetrics();
};
</code></pre>
<p>Now we have our function to publish the metric, we create a handler with a <code>switch</code> statement to route the event to our function.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: EventBridgeEvent&lt;<span class="hljs-string">'DomainEventBase'</span>, DomainEventBase&gt;
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {

  <span class="hljs-keyword">switch</span> (event.detail.metadata.eventType) {
    <span class="hljs-keyword">case</span> EventType.CreditReportFailed:
      publishCreditReportFailedMetrics(event.detail <span class="hljs-keyword">as</span> CreditReportFailedV1);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">default</span>:
      <span class="hljs-keyword">break</span>;
  }
};
</code></pre>
<p>Finally, we add a subscription using the <code>domainEventRule</code> we created earlier.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> measurerFunction = <span class="hljs-keyword">new</span> NodejsFunction(
  <span class="hljs-built_in">this</span>, <span class="hljs-string">'Measurer'</span>, getNodejsFunctionProps()
);

domainEventRule.addTarget(<span class="hljs-keyword">new</span> LambdaFunction(measurerFunction));
</code></pre>
<p>Now that we have a metric for our failures, we can create an alarm to notify ourselves when the failures are occurring. For the purpose of this post, we create a simple alarm that triggers if there is at least one failure in a five minute period.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> creditReportFailedCount = <span class="hljs-keyword">new</span> Metric({
  <span class="hljs-keyword">namespace</span>: OBSERVER_NAMESPACE,
  metricName: CREDIT_REPORT_FAILED_METRIC,
  dimensionsMap: {
    service: OBSERVER_SERVICE_NAME,
  },
}).with({
  statistic: <span class="hljs-string">'sum'</span>,
  period: Duration.minutes(<span class="hljs-number">5</span>),
});

creditReportFailedCount.createAlarm(<span class="hljs-built_in">this</span>, <span class="hljs-string">'CreditReportFailedAlarm'</span>, {
  evaluationPeriods: <span class="hljs-number">1</span>,
  comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
  threshold: <span class="hljs-number">0</span>,
  treatMissingData: TreatMissingData.NOT_BREACHING,
});
</code></pre>
<p>We can repeat this process for any of our domain events, creating metrics and alarms that allow us to observe the business performance of the application.</p>
<h2 id="heading-deriving-business-metrics">Deriving business metrics</h2>
<p>Some business metrics directly correlate with individual business events, such as <code>CreditReportFailed</code> as we saw previously. However, there are some business metrics that do not. One example is the length of time is takes to process a quote. This process is asynchronous and, as a result, there is no single place within the application that could measure this duration.</p>
<p>One solution is to derive such a metric by a combination of events, in this case <code>QuoteSubmitted</code> and <code>QuoteProcessed</code>. By storing the events in a DynamoDB table, indexed by the request id, we can use this table to retrieve the corresponding event and derive the metric.</p>
<p>The first step is to extend the Lambda function that logs the event. In addition to the logging, we now write the event to a DynamoDB table.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: EventBridgeEvent&lt;<span class="hljs-string">'DomainEventBase'</span>, DomainEventBase&gt;
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
  <span class="hljs-built_in">console</span>.log({ ...event.detail.metadata, data: event.detail.data });

  <span class="hljs-comment">// Record the event in DynamoDB</span>
  <span class="hljs-keyword">await</span> requestEventTableClient.putEventAsync(event.detail);
};
</code></pre>
<p>An example for for a single request is shown below.</p>
<p><img src="https://github.com/andybalham/blog-source-code/blob/master/blog-posts/images/ent-int-patterns-with-serverless-and-cdk/request-table-dynamodb-console.png?raw=true" alt="DynamoDB console showing events for a single request" /></p>
<p>The primary key is the <code>requestId</code> and the sort key is a combination of the time the event was received by the logger and the <code>eventId</code>. This allows us to have a chronological view of the events for a particular request. In this example, we can see the requests for rates and their corresponding responses.</p>
<p>Now that we have a searchable log of the events, we can create a Lambda function that will use this to derive our metric. The code for this is shown below.</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">const</span> publishQuoteProcessedMetricsAsync = <span class="hljs-keyword">async</span> (
  quoteProcessed: QuoteProcessedV1
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {

  <span class="hljs-comment">// Retrieve the corresponding event for when the quote was submitted</span>

  <span class="hljs-keyword">const</span> [quoteSubmitted] = <span class="hljs-keyword">await</span> requestEventTableClient.getEventsByType(
    quoteProcessed.metadata.requestId,
    EventType.QuoteSubmitted
  );

  <span class="hljs-comment">// Calculate the duration</span>

  <span class="hljs-keyword">const</span> quoteSubmittedMillis = DateTime.fromISO(
    quoteSubmitted.metadata.timestamp
  ).toMillis();
  <span class="hljs-keyword">const</span> quoteProcessedMillis = DateTime.fromISO(
    quoteProcessed.metadata.timestamp
  ).toMillis();

  <span class="hljs-keyword">const</span> durationMillis = quoteProcessedMillis - quoteSubmittedMillis;

  <span class="hljs-comment">// Publish the metric</span>

  metrics.addMetric(
    QUOTE_PROCESSED_DURATION_METRIC,
    MetricUnits.Milliseconds,
    durationMillis
  );

  addMetadata(quoteProcessed, {
    quoteReference: quoteProcessed.data.quoteReference,
  });

  metrics.publishStoredMetrics();
};
</code></pre>
<p>By hooking this Lambda function up to the <code>quoteProcessed</code> event, it will start emitting metrics for the duration of processing quotes. We could extend this to derive and emit metrics for how long each lender takes to respond, or any other metric that requires correlating multiple events together.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we have seen how we can add business-level observability to our event-driven application. By taking advantage of the decoupled nature of an event-driven architecture, we were able to do this without touching the core application code at all.</p>
]]></content:encoded></item></channel></rss>