<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Arun Stephens]]></title><description><![CDATA[Software development views, news and how-tos from Arun Stephens, a software engineer in New Zealand.]]></description><link>https://arunstephens.com/</link><image><url>https://arunstephens.com/favicon.png</url><title>Arun Stephens</title><link>https://arunstephens.com/</link></image><generator>Ghost 5.88</generator><lastBuildDate>Tue, 14 Apr 2026 16:13:07 GMT</lastBuildDate><atom:link href="https://arunstephens.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Credit where credit's due - but what about the developers?]]></title><description><![CDATA[<p>My team, along with a couple of other teams, recently released a new feature at work. This is a cause for celebration! The release was marked with some internal comms that thanked the people who worked on it: project and product management, marketing and customer people.</p><p>Engineers? Nah, they did</p>]]></description><link>https://arunstephens.com/credit-where-credits-due-but-what-about-the-devs/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c77c</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 25 Mar 2024 03:35:40 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1521791136064-7986c2920216?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fHNoYWtpbmclMjBoYW5kc3xlbnwwfHx8fDE3MTEzMjgxMDd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1521791136064-7986c2920216?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fHNoYWtpbmclMjBoYW5kc3xlbnwwfHx8fDE3MTEzMjgxMDd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Credit where credit&apos;s due - but what about the developers?"><p>My team, along with a couple of other teams, recently released a new feature at work. This is a cause for celebration! The release was marked with some internal comms that thanked the people who worked on it: project and product management, marketing and customer people.</p><p>Engineers? Nah, they did nothing!</p><p>This got me thinking about my days working as a dev in digital agencies in London.   There were a few massive campaigns that we worked on. Some of them won awards or were featured in industry publications. The same thing happened. The creatives would get a mention. The project manager would get a mention. The account manager would definitely get a mention. The technical director? Possibly. But the three or four devs who worked to meet a seemingly impossibly tight deadline that was agreed to by someone who didn&apos;t even ask them how long it might take? Nothing.</p><p>I think it&apos;s normal practice that the people who manage the teams that deliver work to get the public credit for it. A good manager will pass those congratulations onto their reports who did the work. And that&apos;s what happened in this recent case at work, so we&apos;re all good there!</p><p>But if the managers deserve direct public praise for the work they delivered with the help of their team, why don&apos;t the team members themselves? There seems to be no good reason, other than there are usually a lot of them.</p><p>Going back to that agency example, what struck me at the time was that it wasn&apos;t just senior managers getting that public attention. The designers that designed the work were mentioned. The devs that implemented those designs were at the same level on the hierarchy. That just seemed unfair.</p><p>If you look at the audience for these types of message, and the authors of the messages, you can begin to see why the technical teams that delivered the work might get missed out. For the internal comms about a product release, they focus, quite rightly, on the changes to the product and how they improve the customer&apos;s experience using it. And the first people you think about when you&apos;re thinking about the product and customers are product and customer people!</p><p>In agency world, maybe it comes down to the fact that they have awards for creative arts, but they don&apos;t give out any awards for configuring Akamai to set a cookie using its directives, rather than overloading the single Windows Server 2003 box that serves all your clients&apos; microsites, so that you can personalise banner ads featuring space hoppers in order to sell more mobile phones.</p><p>What do you think? Are we, as an industry, giving credit where credit&apos;s due, or are we guilty of unconscious bias when thinking about how work got done?</p><div class="kg-card kg-signup-card kg-width-wide " data-lexical-signup-form style="background-color: #F0F0F0; display: none;">
            
            <div class="kg-signup-card-content">
                
                <div class="kg-signup-card-text ">
                    <h2 class="kg-signup-card-heading" style="color: #000000;"><span style="white-space: pre-wrap;">Sign up for Arun Stephens</span></h2>
                    <p class="kg-signup-card-subheading" style="color: #000000;"><span style="white-space: pre-wrap;">Software development views, news and how-tos from Arun Stephens, a software engineer in New Zealand.</span></p>
                    
        <form class="kg-signup-card-form" data-members-form="signup">
            
            <div class="kg-signup-card-fields">
                <input class="kg-signup-card-input" id="email" data-members-email type="email" required="true" placeholder="Your email">
                <button class="kg-signup-card-button kg-style-accent" style="color: #FFFFFF;" type="submit">
                    <span class="kg-signup-card-button-default">Subscribe</span>
                    <span class="kg-signup-card-button-loading"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewbox="0 0 24 24">
        <g stroke-linecap="round" stroke-width="2" fill="currentColor" stroke="none" stroke-linejoin="round" class="nc-icon-wrapper">
            <g class="nc-loop-dots-4-24-icon-o">
                <circle cx="4" cy="12" r="3"/>
                <circle cx="12" cy="12" r="3"/>
                <circle cx="20" cy="12" r="3"/>
            </g>
            <style data-cap="butt">
                .nc-loop-dots-4-24-icon-o{--animation-duration:0.8s}
                .nc-loop-dots-4-24-icon-o *{opacity:.4;transform:scale(.75);animation:nc-loop-dots-4-anim var(--animation-duration) infinite}
                .nc-loop-dots-4-24-icon-o :nth-child(1){transform-origin:4px 12px;animation-delay:-.3s;animation-delay:calc(var(--animation-duration)/-2.666)}
                .nc-loop-dots-4-24-icon-o :nth-child(2){transform-origin:12px 12px;animation-delay:-.15s;animation-delay:calc(var(--animation-duration)/-5.333)}
                .nc-loop-dots-4-24-icon-o :nth-child(3){transform-origin:20px 12px}
                @keyframes nc-loop-dots-4-anim{0%,100%{opacity:.4;transform:scale(.75)}50%{opacity:1;transform:scale(1)}}
            </style>
        </g>
    </svg></span>
                </button>
            </div>
            <div class="kg-signup-card-success" style="color: #000000;">
                Email sent! Check your inbox to complete your signup.
            </div>
            <div class="kg-signup-card-error" style="color: #000000;" data-members-error></div>
        </form>
        
                    <p class="kg-signup-card-disclaimer" style="color: #000000;"><span style="white-space: pre-wrap;">No spam. Unsubscribe anytime.</span></p>
                </div>
            </div>
        </div>]]></content:encoded></item><item><title><![CDATA[Viral times at Hang Five]]></title><description><![CDATA[<p>It&apos;s been a busy week at Hang Five. I&apos;ve written about this a little in the <a href="https://hangfive.substack.com/p/its-great-to-have-you-here?ref=arunstephens.com">Hang Five newsletter</a> (yes, there&apos;s a Hang Five newsletter, please subscribe) but I&apos;ve got more of a behind-the-scenes take here.</p><p>We have been trying to figure</p>]]></description><link>https://arunstephens.com/viral-times-at-hang-five/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c77b</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Fri, 26 Jan 2024 04:17:09 GMT</pubDate><media:content url="https://arunstephens.com/content/images/2024/01/20240126_171332.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://arunstephens.com/content/images/2024/01/20240126_171332.jpg" alt="Viral times at Hang Five"><p>It&apos;s been a busy week at Hang Five. I&apos;ve written about this a little in the <a href="https://hangfive.substack.com/p/its-great-to-have-you-here?ref=arunstephens.com">Hang Five newsletter</a> (yes, there&apos;s a Hang Five newsletter, please subscribe) but I&apos;ve got more of a behind-the-scenes take here.</p><p>We have been trying to figure out a way to approach media to spread the word about Hang Five. We took a bit of a break over Christmas, and hadn&apos;t really gotten back onto the promotion track. But the promotion track found us!</p><p>A while back, one of my friends shared Hang Five with a group of her friends. One of them subscribed to <a href="https://shityoushouldcareabout.com/?ref=arunstephens.com" rel="noreferrer">Shit You Should Care About</a>, an immensely popular (although unknown to me at the time) <a href="https://shityoushouldcareabout.substack.com/?ref=arunstephens.com" rel="noreferrer">newsletter</a> and <a href="https://instagram.com/shityoushouldcareabout?ref=arunstephens.com" rel="noreferrer">Instagram</a> account that digests news and culture to a primarily gen Z audience. Luce, who writes the newsletter and runs the independent media company, has been playing it with her family for the last wee while and decided to share it with her Close Friends list on Instagram.</p><div class="kg-card kg-signup-card kg-width-regular " data-lexical-signup-form style="background-color: #F0F0F0; display: none;">
            
            <div class="kg-signup-card-content">
                
                <div class="kg-signup-card-text ">
                    <h2 class="kg-signup-card-heading" style="color: #000000;"><span style="white-space: pre-wrap;">Sign up to my newsletter</span></h2>
                    <p class="kg-signup-card-subheading" style="color: #000000;"><span style="white-space: pre-wrap;">Software development views, news and how-tos from Arun Stephens, a software engineer in New Zealand.</span></p>
                    
        <form class="kg-signup-card-form" data-members-form="signup">
            
            <div class="kg-signup-card-fields">
                <input class="kg-signup-card-input" id="email" data-members-email type="email" required="true" placeholder="Your email">
                <button class="kg-signup-card-button kg-style-accent" style="color: #FFFFFF;" type="submit">
                    <span class="kg-signup-card-button-default">Subscribe</span>
                    <span class="kg-signup-card-button-loading"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewbox="0 0 24 24">
        <g stroke-linecap="round" stroke-width="2" fill="currentColor" stroke="none" stroke-linejoin="round" class="nc-icon-wrapper">
            <g class="nc-loop-dots-4-24-icon-o">
                <circle cx="4" cy="12" r="3"/>
                <circle cx="12" cy="12" r="3"/>
                <circle cx="20" cy="12" r="3"/>
            </g>
            <style data-cap="butt">
                .nc-loop-dots-4-24-icon-o{--animation-duration:0.8s}
                .nc-loop-dots-4-24-icon-o *{opacity:.4;transform:scale(.75);animation:nc-loop-dots-4-anim var(--animation-duration) infinite}
                .nc-loop-dots-4-24-icon-o :nth-child(1){transform-origin:4px 12px;animation-delay:-.3s;animation-delay:calc(var(--animation-duration)/-2.666)}
                .nc-loop-dots-4-24-icon-o :nth-child(2){transform-origin:12px 12px;animation-delay:-.15s;animation-delay:calc(var(--animation-duration)/-5.333)}
                .nc-loop-dots-4-24-icon-o :nth-child(3){transform-origin:20px 12px}
                @keyframes nc-loop-dots-4-anim{0%,100%{opacity:.4;transform:scale(.75)}50%{opacity:1;transform:scale(1)}}
            </style>
        </g>
    </svg></span>
                </button>
            </div>
            <div class="kg-signup-card-success" style="color: #000000;">
                Email sent! Check your inbox to complete your signup.
            </div>
            <div class="kg-signup-card-error" style="color: #000000;" data-members-error></div>
        </form>
        
                    <p class="kg-signup-card-disclaimer" style="color: #000000;"><span style="white-space: pre-wrap;">No spam. Unsubscribe anytime.</span></p>
                </div>
            </div>
        </div><p>So my friend saw that Story, and shared it with me. I checked all our dashboards and we started to see an uptick in traffic. This was last Thursday.</p><p>I emailed Luce and thanked her for playing and for sharing. And said she could suggest a topic if she wanted. She immediately came back with the BEST topic for her audience. It was a coincidence (or was it) that we&apos;re both NZ-based as well. </p><p>Fast forward to this Thursday, and the unofficial <em>Hang Five x Shit You Should Care About </em>collab went live. And traffic skyrocketed! Over 4,000 played yesterday&apos;s game. That&apos;s insane! </p><p>Our new audience is a bit different to our traditional one. Most people who have arrived at Hang Five have found it from a personal recommendation. Friends who play it and have recommended it. There&apos;s a real social aspect to it. There&apos;s a Share button, and around 20% of players tend to click it each day.</p><p>On Thursday that share rate dropped to 8%! And I think that&apos;s due to the relationship between the person that recommended it and the new player not being as strong as if it were someone you saw every day.</p><p>We&apos;re eagerly awaiting the next few days to see how many of those 4,000 people stick around and keep playing! </p><p>We&apos;ve run test ads on LinkedIn, Facebook and Google in the past. Retention of those groups is nowhere near as good as the retention of the word of mouth players. I expect that retention of our new SYSCAhood players will be somewhere in the middle.</p><p>Some other experiments I&apos;ve been looking at are to do with new user onboarding. I noticed this with the ads, that a lot of people weren&apos;t finishing their first game. I built a little walkthrough to see if it would make a difference. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://arunstephens.com/content/images/2024/01/Untitled-1-2.png" class="kg-image" alt="Viral times at Hang Five" loading="lazy" width="304" height="592"><figcaption><span style="white-space: pre-wrap;">Screenshot of Hang Five walkthrough</span></figcaption></figure><p>It did. But not much. I&apos;ve still got it running in an A/B test, but it&apos;s probably time to switch off the little tooltips because they don&apos;t make too much difference. The funnel is Start game -&gt; Choose first letter -&gt; Finish game. I&apos;ve found that the first step conversion is better with the tooltips, but the whole funnel&apos;s conversion is almost the same. </p><p>Next time (not sure when that will be) I&apos;ll write a bit more about the technology behind Hang Five.</p><p>Have a great weekend!</p><p>Arun</p>]]></content:encoded></item><item><title><![CDATA[Some stats about Hang Five]]></title><description><![CDATA[<p>I thought I&apos;d quickly share some stats about Hang Five.</p><p>We are now at game #100. It&apos;s actually our 101st game, because we started at 0!</p><p>In the past 100 days, over 2,900 people have played over 22,000 times!</p><p>People love it. 50% of</p>]]></description><link>https://arunstephens.com/some-stats-about-hang-five/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c77a</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Tue, 14 Nov 2023 08:30:16 GMT</pubDate><media:content url="https://arunstephens.com/content/images/2023/11/Frame-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://arunstephens.com/content/images/2023/11/Frame-1.png" alt="Some stats about Hang Five"><p>I thought I&apos;d quickly share some stats about Hang Five.</p><p>We are now at game #100. It&apos;s actually our 101st game, because we started at 0!</p><p>In the past 100 days, over 2,900 people have played over 22,000 times!</p><p>People love it. 50% of yesterday&apos;s players played it 4 times or more in the past week. 23% of them have played 7 days in a row!</p><p>The easiest game so far was actually yesterday, #99 (&quot;... and chips&quot;), with 97.61% of people solved it. The hardest one was #19 (&quot;Backstreet&apos;s Back, alright&quot;, back in August) when only 9.09% of people solved it.</p><p>In the past month, 24% of people have clicked the &quot;Share&quot; button to share with their friends.</p><p>And if you search for Hang Five, in New Zealand at least, we&apos;re the number 1 result on Google.</p><p>We&apos;re about to start our media blitz, both in New Zealand and overseas. If you&apos;re a journalist or influencer who might be interested in sharing Hang Five with your audience, please <a href="mailto:hello@playhangfive.com" rel="noreferrer">get in touch</a>!</p>]]></content:encoded></item><item><title><![CDATA[Introducing Hang Five, your new favourite word game]]></title><description><![CDATA[<p>My wife Amy and I recently went to Indonesia on a trip around Bali, Flores and Lombok. Amy bought a few word puzzle books at the airport in Wellington. We needed something to occupy our time during our 6 hour layover in Darwin. We played them every now and then.</p>]]></description><link>https://arunstephens.com/introducing-hang-five/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c778</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 28 Aug 2023 08:32:44 GMT</pubDate><media:content url="https://arunstephens.com/content/images/2023/08/New-Product-Promotion-Mobile-Phone-Mockup-Facebook-Cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://arunstephens.com/content/images/2023/08/New-Product-Promotion-Mobile-Phone-Mockup-Facebook-Cover.png" alt="Introducing Hang Five, your new favourite word game"><p>My wife Amy and I recently went to Indonesia on a trip around Bali, Flores and Lombok. Amy bought a few word puzzle books at the airport in Wellington. We needed something to occupy our time during our 6 hour layover in Darwin. We played them every now and then. But we thought, could we invent our own game? Something that&apos;s sticky, and weirdly competitive, like Wordle?</p><p>We came up with an idea while we were in Indonesia, and when we got home, set about making the game. And thus, Hang Five was born!</p><p>We tested it with friends for a couple of weeks before getting the game mechanics right. It started off too easy, then too hard, and now, we think, it&apos;s just right!</p><p>Check it out at playhangfive.com. Let me know how you get on! And if you become addicted, you&apos;ll have to wait till midnight till the next puzzle&apos;s available.</p>]]></content:encoded></item><item><title><![CDATA[Will Meta's Threads be successful?]]></title><description><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4C5;</div><div class="kg-callout-text">They seem to have brought the launch forward. It&apos;s now saying Thursday 11am NZT!</div></div><p>On Friday at 2am (NZT), we&apos;ll get to see Meta&apos;s not-that-long-awaited Twitter killer, called Threads. It&apos;s already available for pre-order on the <a href="https://apps.apple.com/us/app/threads-an-instagram-app/id6446901002?ref=arunstephens.com">App Store</a>, and there&apos;</p>]]></description><link>https://arunstephens.com/will-metas-threads-be-successful/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c776</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Wed, 05 Jul 2023 01:29:35 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1547042591-aae98619aab5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHRocmVhZHxlbnwwfHx8fDE2ODg1MTkyNjd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4C5;</div><div class="kg-callout-text">They seem to have brought the launch forward. It&apos;s now saying Thursday 11am NZT!</div></div><img src="https://images.unsplash.com/photo-1547042591-aae98619aab5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHRocmVhZHxlbnwwfHx8fDE2ODg1MTkyNjd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Will Meta&apos;s Threads be successful?"><p>On Friday at 2am (NZT), we&apos;ll get to see Meta&apos;s not-that-long-awaited Twitter killer, called Threads. It&apos;s already available for pre-order on the <a href="https://apps.apple.com/us/app/threads-an-instagram-app/id6446901002?ref=arunstephens.com">App Store</a>, and there&apos;s a <a href="https://play.google.com/store/apps/details?id=com.instagram.barcelona&amp;gl=it&amp;pli=1&amp;ref=arunstephens.com">Google Play</a> listing as well. It seems it had the code name &quot;Barcelona&quot; internally.</p><p>Its home on the web will be <a href="https://threads.net/?ref=arunstephens.com">threads.net</a>, which is currently a countdown with a 3D animation of the Threads logo, in Instagram colours, that you can rotate. </p><p>There&apos;s also an easter egg of sorts in the Instagram app. If you search for &quot;threads&quot; a little ticket icon will appear in the search bar. Click that and you&apos;ll get your personal invitation to the launch. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://arunstephens.com/content/images/2023/07/Screenshot_20230705_130904_Instagram-1.jpg" class="kg-image" alt="Will Meta&apos;s Threads be successful?" loading="lazy" width="320" height="693"><figcaption>My invitation to Threads</figcaption></figure><p>But will it be successful?</p><p>Well, the timing of the launch can&apos;t be a coincidence. The rate limiting fiasco over the last weekend caused a lot of discontent amongst the people that I follow. And the people I don&apos;t, for that matter - even the algorithmically-generated &quot;for you&quot; section had a lot of Twitter hate in it. People have been looking for a place to go for a while. Mentions of people wanting Blue Sky invitations were going up. </p><p>Since news of the Threads launch started surfacing over the past 24 hours, one thing that has caught people&apos;s eye is the privacy policy on the App Store listing. It has a list of &quot;Data Linked to You&quot; but it&apos;s actually the same as the list for the Facebook and Instagram apps. Yes, a concern, but no more a concern than it already has been. Admittedly, Twitter has a smaller list here.</p><p>Apparently Threads will use the ActivityPub spec, which is what Mastodon is based on. If they federate, it might finally be an easy way for the masses (me included) to get onto the fediverse. </p><p>I was a very early adopter of Instagram, being able to nab the <a href="https://www.instagram.com/arun/?ref=arunstephens.com">@arun</a> username. That means I get to keep it for Threads as well. And your followers will also come along for the ride. For some reason (and I should try and figure out how this came to be the case) I have over 17,000 followers on Instagram. I get to keep them. They were probably pretty disappointed Instagram followers, given the content I posted. They&apos;ll be even more disappointed reading my text-only content I&apos;m sure. </p><p>I expect that there are a lot of people on Twitter who aren&apos;t on any Meta properties, so it will be interesting to see whether they join this one. Instagram skews young, as well, so will Threads be the same? </p><p>I will give it a go, and see if I can capitalise on the 17k followers that I&apos;ll bring with me. Surely some of them will be massive nerds that have a vague interest in what I say!</p><p>What about you? Are you going to sign up? Let me know in the comments, on <a href="https://twitter.com/arunstephens?ref=arunstephens.com">Twitter</a> or, once it&apos;s launched, on Threads. I&apos;m assuming I&apos;ll be at <a href="https://threads.net/arun?ref=arunstephens.com">https://threads.net/arun</a>.</p>]]></content:encoded></item><item><title><![CDATA[Will AI kill factual writing?]]></title><description><![CDATA[<p>We&apos;ve been living in a post-LLM world for about 6 months now. Some are saying that the hype is dying down. Search trends for ChatGPT are dropping the same way that searches for NFTs dropped off (thankfully). </p><p>When thinking of topics when as I was starting out this</p>]]></description><link>https://arunstephens.com/will-ai-kill-factual-writing/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c773</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 26 Jun 2023 23:24:13 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1613923339596-bd8f3ec7abd5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDU1fHxyb2JvdCUyMHxlbnwwfHx8fDE2ODc4MjE3OTN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1613923339596-bd8f3ec7abd5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDU1fHxyb2JvdCUyMHxlbnwwfHx8fDE2ODc4MjE3OTN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Will AI kill factual writing?"><p>We&apos;ve been living in a post-LLM world for about 6 months now. Some are saying that the hype is dying down. Search trends for ChatGPT are dropping the same way that searches for NFTs dropped off (thankfully). </p><p>When thinking of topics when as I was starting out this newsletter a few months ago, one that was near the top of my list was this one: will AI kill factual writing? I was drawn to it because I thought that a lot of the prose around writing about technical topics could probably artificially generated, and be correct. Or at least, correct with minor edits required.</p><p>What I mean by factual writing is the human author coming up with the facts about something, and letting AI figure out how the present them nicely for the reader. Obviously I&apos;m coming at this from a technical standpoint, but perhaps the same could apply to an investigative journalist writing a summary of their findings and having AI write the article.</p><p>Last week I learnt something new at work. I shared my findings with my team in Slack, and joked that I could probably get AI to write a blog post about it. </p><p>See for yourself. I put Bing into &quot;More Creative&quot; mode and entered this prompt:</p><blockquote>Write a blog post in the style of Arun Stephens (arunstephens.com) about this: I wanted to find out which other repos reference <code>MyProduct.Business.Contracts.MyContract</code> &#xA0;and the GitHub web UI wasn&apos;t cutting it. So I installed the <code>gh</code> command line app, auth&apos;d using <code>gh auth login</code> and then ran <code>gh api --paginate search/code?q=MyProduct.Business.Contracts.MyContract | jq .items[].repository.full_name | sort | uniq</code> Gave me a sorted list of repos that mention that in the code!</blockquote><p>I&apos;ve put <a href="https://arunstephens.com/how-to-find-all-the-repos-that-reference-a-specific-contract-using-github-cli/">the result in its own post</a>. It&apos;s not bad at all. It&apos;s probably something that I might have written. It&apos;s also correctly expanded the context as to why wanted to find references to that class. I would have not concentrated on it being &quot;a specific contract&quot; though, as the search could be much wider than that.</p><p>To be fair, this isn&apos;t a particularly complex thing to write about. It&apos;s also not particularly novel. There&apos;s also a very strong argument that the original Slack post was better because it was more concise. You definitely get to the answer quicker. Maybe it wasn&apos;t necessary to expand this into an explainer blog post at all.</p><p>Could AI kill factual writing? </p><p>Certainly!</p><p>Will it? </p><p>No.</p><p>It may well reduce it, but there will definitely be space for novel writing to continue. And I think, well, hope, that society will still value human-written content, even if the machine-generated kind may not be distinguishable from it.</p><p>So here&apos;s what Bing (which is GPT behind the scenes) turned my little Slack post into:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://arunstephens.com/how-to-find-all-the-repos-that-reference-a-specific-contract-using-github-cli/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">How to find all the repos that reference a specific contract using GitHub CLI</div><div class="kg-bookmark-description">&#x1F916;This article was written by AI. For more context, see the post I&#x2019;m about to put up. If you work with a large codebase that spans multiple repositories, you might have encountered the problem of finding all the places where a certain contract or interface is used. For example, I</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://arunstephens.com/favicon.ico" alt="Will AI kill factual writing?"><span class="kg-bookmark-author">Arun Stephens</span><span class="kg-bookmark-publisher">Arun Stephens</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://images.unsplash.com/photo-1467842976472-03f46c6b1e3d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fGhheXN0YWNrfGVufDB8fHx8MTY4NzgyMTUxNXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Will AI kill factual writing?"></div></a></figure><p>What do you think? Let me know in the comments, on <a href="https://www.linkedin.com/in/arunstephens/?ref=arunstephens.com">LinkedIn</a> or on <a href="https://twitter.com/arunstephens?ref=arunstephens.com">Twitter</a>.</p>]]></content:encoded></item><item><title><![CDATA[How to find all the repos that reference a specific contract using GitHub CLI]]></title><description><![CDATA[If you work with a large codebase that spans multiple repositories, you might have encountered the problem of finding all the places where a certain contract or interface is used.]]></description><link>https://arunstephens.com/how-to-find-all-the-repos-that-reference-a-specific-contract-using-github-cli/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c774</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 26 Jun 2023 23:19:26 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1467842976472-03f46c6b1e3d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fGhheXN0YWNrfGVufDB8fHx8MTY4NzgyMTUxNXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F916;</div><div class="kg-callout-text">This article was written by AI. For more context, see my post <a href="https://arunstephens.com/will-ai-kill-factual-writing/"><em>Will AI kill factual writing?</em></a></div></div><img src="https://images.unsplash.com/photo-1467842976472-03f46c6b1e3d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fGhheXN0YWNrfGVufDB8fHx8MTY4NzgyMTUxNXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="How to find all the repos that reference a specific contract using GitHub CLI"><p>If you work with a large codebase that spans multiple repositories, you might have encountered the problem of finding all the places where a certain contract or interface is used. For example, I wanted to find out which other repos reference <code>MyProduct.Business.Contracts.MyContract</code> and the GitHub web UI wasn&#x2019;t cutting it.</p><p>Sure, you can use the web UI to search for code, but it has some limitations. You can only see 10 results per page, and you have to click through each result to see the repository name. Also, you might get some false positives from forks or archived repos that are not relevant.</p><p>So I decided to try a different approach: using the GitHub CLI tool. The GitHub CLI is a command-line interface that lets you interact with GitHub from your terminal. You can do things like create issues, pull requests, releases, and more. But you can also use it to query the GitHub API and get JSON data back.</p><p>One of the commands that the GitHub CLI supports is <code>gh api</code>, which allows you to make arbitrary requests to the GitHub API. You can pass in any endpoint and parameters, and get the response as JSON. You can also use the <code>--paginate</code> flag to automatically fetch all pages of results.</p><p>So I installed the gh command line app, auth&#x2019;d using <code>gh auth login</code> and then ran this command:</p><pre><code class="language-bash">gh api --paginate search/code?q=MyProduct.Business.Contracts.MyContract | jq .items[].repository.full_name | sort | uniq
</code></pre><p>This command does the following:</p><ul><li>It uses the <code>search/code</code> endpoint to search for code that matches <code>MyProduct.Business.Contracts.MyContract</code>.</li><li>It pipes the JSON output to <code>jq</code>, a command-line tool for processing JSON data.</li><li>It extracts the <code>full_name</code> property of each repository from the <code>items</code> array using <code>jq .items[].repository.full_name</code>.</li><li>It sorts the results alphabetically using <code>sort</code>.</li><li>It removes any duplicates using <code>uniq</code>.</li></ul><p>The result is a sorted list of repos that mention that contract in the code. Here&#x2019;s an example output:</p><pre><code class="language-bash">arunstephens/MyProduct.Api
arunstephens/MyProduct.Client
arunstephens/MyProduct.Common
arunstephens/MyProduct.Service
</code></pre><p>This way, I can quickly see all the repos that depend on that contract, and make any changes or updates accordingly. I can also use other filters or modifiers in the query string, such as <code>language</code>, <code>user</code>, <code>org</code>, etc.</p><p>I hope you found this tip useful. If you want to learn more about the GitHub CLI, check out their <a href="https://cli.github.com/manual/?ref=arunstephens.com">documentation</a>. And if you have any questions or feedback, feel free to leave a comment below or reach out to me on <a href="https://twitter.com/arunstephens?ref=arunstephens.com">Twitter</a>.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://arunstephens.com/will-ai-kill-factual-writing/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Will AI kill factual writing?</div><div class="kg-bookmark-description">We&#x2019;ve been living in a post-LLM world for about 6 months now. Some are saying that the hype is dying down. Search trends for ChatGPT are dropping the same way that searches for NFTs dropped off (thankfully). When thinking of topics when as I was starting out this newsletter a</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://arunstephens.com/favicon.ico" alt="How to find all the repos that reference a specific contract using GitHub CLI"><span class="kg-bookmark-author">Arun Stephens</span><span class="kg-bookmark-publisher">Arun Stephens</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://images.unsplash.com/photo-1613923339596-bd8f3ec7abd5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDU1fHxyb2JvdCUyMHxlbnwwfHx8fDE2ODc4MjE3OTN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="How to find all the repos that reference a specific contract using GitHub CLI"></div></a><figcaption>Find out more on how this article came to be</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Exploring Native Ahead Of Time Compilation in .NET: Pros, Cons, and Possibilities]]></title><description><![CDATA[<p>Before code written by a programmer can be executed by a computer, it needs to go through a process called compilation. It takes what a human (well, some of us) understands, and turns it into something the computer understands. </p><p>In .NET, there are technically two steps in this compilation. The</p>]]></description><link>https://arunstephens.com/exploring-native-aot-in-dotnet/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c772</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 19 Jun 2023 05:39:39 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1640682841767-cdfce3aea6e0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHRvb2xib3h8ZW58MHx8fHwxNjg3MTUyOTM0fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1640682841767-cdfce3aea6e0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHRvb2xib3h8ZW58MHx8fHwxNjg3MTUyOTM0fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Exploring Native Ahead Of Time Compilation in .NET: Pros, Cons, and Possibilities"><p>Before code written by a programmer can be executed by a computer, it needs to go through a process called compilation. It takes what a human (well, some of us) understands, and turns it into something the computer understands. </p><p>In .NET, there are technically two steps in this compilation. The first is when the source code (in C#, say) is compiled in to <a href="https://en.wikipedia.org/wiki/Common_Intermediate_Language?ref=arunstephens.com">Intermediary Language (IL)</a>. The second occurs at runtime, when the <a href="https://en.wikipedia.org/wiki/Common_Language_Runtime?ref=arunstephens.com">Common Language Runtime (CLR)</a>, the framework that your code runs on top of, translates it into the machine code that the computer can understand. This last step happens just before it needs to be run by the computer. It is called Just In Time compilation, or JIT.</p><p>Another approach is to translate it the whole way up front, before you need to run it. This is Ahead Of Time compilation, or AOT. AOT is not new. Languages like C and Pascal are compiled ahead of time.</p><p>Native AOT is now here for .NET. What does it bring to the table? </p><ul><li>You don&apos;t need to have the .NET runtime installed to run your app. Just package up your app and it will run.</li><li>Native AOT apps can also run in environments that don&apos;t allow JIT, typically embedded systems.</li><li>They are supposed to use less memory and be faster to start up. This can make a difference with applications that scale out to have multiple instances. </li></ul><h2 id="writing-a-simple-aot-app">Writing a simple AOT app</h2><p>There&apos;s a decent <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?ref=arunstephens.com#limitations-of-native-aot-deployment">list of things that can&apos;t be done with Native AOT</a>, one of which is using ASP.NET MVC, so our first AOT app will have to use minimal APIs. </p><p>We&apos;ll start by creating a new app. We&apos;ll just start with the standard Hello World app generated by the <code>dotnet</code> command.</p><pre><code class="language-powershell">PS C:\src&gt; dotnet new web -o AotMinimal
The template &quot;ASP.NET Core Empty&quot; was created successfully.

Processing post-creation actions...
Restoring C:\src\AotMinimal\AotMinimal.csproj:
  Determining projects to restore...
  Restored C:\src\AotMinimal\AotMinimal.csproj (in 99 ms).
Restore succeeded.</code></pre><p>This creates our base project. The <code>Program.cs</code> file looks like this:</p><pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet(&quot;/&quot;, () =&gt; &quot;Hello World!&quot;);

app.Run();</code></pre><p>That just returns a simple string. Can&apos;t be simpler than this, can it?</p><p>Let&apos;s make it a bit more complicated, and add this endpoint:</p><pre><code class="language-csharp">app.MapGet(&quot;/object&quot;, () =&gt; new { message = &quot;Hello World!&quot; });</code></pre><p>We&apos;ve now got a simple app. If we run it with <code>dotnet run</code> we can hit our <code>/</code> endpoint and receive <code>Hello World!</code> as a string. If we hit <code>/object</code> we&apos;ll get a JSON string <code>{&quot;message&quot;:&quot;Hello World!&quot;}</code>.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F914;</div><div class="kg-callout-text">Hmmm... I wonder if adding this bit of complication might bite us in the butt later on?</div></div><h2 id="building-and-publishing">Building and publishing</h2><p>We&apos;re going to deploy our application into a containerised environment like Docker or Kubernetes. </p><h4 id="building-the-jit-version">Building the JIT version</h4><p>Let&apos;s start with a standard JIT configuration.</p><pre><code class="language-Dockerfile">FROM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build-env
WORKDIR /App

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0-preview
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT [&quot;dotnet&quot;, &quot;AotMinimal.dll&quot;]</code></pre><p>This is a standard .NET Dockerfile. It sets up a build environment using the SDK image, copies the source code into the container, restores NuGet packages, and builds the project, placing the output into the <code>out</code> directory.</p><p>It then creates the rutime image, taking the <code>aspnet</code> image as a base, copies the <code>out</code> directory from the build environment, then sets the entry point to run the application.</p><p>We can build this by running:</p><pre><code>docker build . --tag minimal:jit</code></pre><p>And run it by running:</p><pre><code>docker run -it --rm -p 8080:8080 minimal:jit</code></pre><p>You can then hit <code>http://localhost:8080</code> to receive the basic Hello World message, or <code>http://localhost:8080/object</code> to receive the JSON object.</p><h3 id="building-the-aot-version">Building the AOT version</h3><p>First up, we&apos;ll need to edit the csproj file to enable AOT. We do this by adding this setting:</p><pre><code class="language-xml">&lt;PropertyGroup&gt;
    &lt;PublishAot&gt;true&lt;/PublishAot&gt;
&lt;/PropertyGroup&gt;</code></pre><p>If we try building it using the existing Dockerfile, we get errors. There are a few, but the important one for now is:</p><pre><code>#0 16.10 /root/.nuget/packages/microsoft.dotnet.ilcompiler/8.0.0-preview.5.23280.8/build/Microsoft.NETCore.Native.Unix.targets(178,5): error : Platform linker (&apos;clang&apos; or &apos;gcc&apos;) not found in PATH. Ensure you have all the required prerequisites documented at https://aka.ms/nativeaot-prerequisites. [/App/AotMinimal.csproj]</code></pre><p>The build environment doesn&apos;t have everything it needs to build AOT applications. We&apos;ll need to change the Dockerfile like so:</p><pre><code class="language-Dockerfile">FROM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build-env
WORKDIR /App

# Install NativeAOT build prerequisites
RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends \
       clang zlib1g-dev

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -r linux-x64 -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-preview
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT [&quot;/App/AotMinimal&quot;]</code></pre><p>This one uses the same base image as the build environment, we first <a href="https://aka.ms/nativeaot-prerequisites?ref=arunstephens.com">need to install the <code>clang</code> and <code>zlib1g-dev</code> packages</a>. This installs the C compiler and the zlib compression library.</p><p>The second change is to the &#xA0;<code>run dotnet publish</code> command. Note the <code>-r linux-x64</code> option. That&apos;s telling it to build for the linux-x64 runtime. AOT apps have to be build for the specific architecture they&apos;re running on - that&apos;s the whole point!</p><p>Finally, the runtime image has changed. It&apos;s not the .NET runtime anymore, but the (much smaller) .NET runtime deps image. This image doesn&apos;t contain the .NET runtime, just the things that .NET AOT apps need to run, which is a fairly standard Linux installation. We also need to change the entry point to be simply run the <code>AotMinimal</code> command. We don&apos;t need to run it with the <code>dotnet</code> command, because it doesn&apos;t need it. It&apos;s a runnable Linux executable.</p><p>We can build this by running:</p><pre><code>docker build . --tag minimal:aot</code></pre><p>We still get warnings here, but it is successful. Let&apos;s ignore the warnings for now (mainly because by default they get hidden in the output of the <code>docker build</code> comand), and try running it by running:</p><pre><code>docker run -it --rm -p 8080:8080 minimal:aot</code></pre><p>It seems to start well, but what if we hit <code>http://localhost:8080/</code>? </p><p>Oh no! We get an error!</p><pre><code>fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id &quot;0HMRGDSJK9IOC&quot;, Request id &quot;0HMRGDSJK9IOC:00000001&quot;: An unhandled exception was thrown by the application.
      System.InvalidOperationException: JsonSerializerOptions instance must specify a TypeInfoResolver setting before being marked as read-only.</code></pre><p>And this brings us to the main thing I&apos;ve discovered about .NET Native AOT! You&apos;re not going to be able to use AOT using all the tools and techniques you use today.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F680;</div><div class="kg-callout-text">Check out the code examples from this article at <a href="https://github.com/arunstephens/aot-minimal?ref=arunstephens.com">https://github.com/arunstephens/aot-minimal</a>.</div></div><h2 id="why-isnt-it-working">Why isn&apos;t it working?</h2><p>Remember those compiler warnings I mentioned before? Let&apos;s take a look at them now. To make them stick around after your build completes, add <code>--progress plain</code> to the end of your <code>docker build</code>command. The errors are extensive!</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close"><div class="kg-toggle-heading"><h4 class="kg-toggle-heading-text">Build errors</h4><button class="kg-toggle-card-icon"><svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"/></svg></button></div><div class="kg-toggle-content"><p>#12 6.230 /App/Program.cs(5,1): warning RDG004: Unable to resolve anonymous return type. Compile-time endpoint generation will skip this endpoint and the endpoint will be generated at runtime. For more information, please see https://aka.ms/aot-known-issues [/App/AotMinimal.csproj]</p><p>#12 6.230 /App/Program.cs(5,1): warning IL3050: Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder, String, Delegate)&apos; which has &apos;RequiresDynamicCodeAttribute&apos; can break functionality when AOT compiling. This API may perform reflection on the supplied delegate and its parameters. These types may require generated code and aren&apos;t compatible with native AOT applications. [/App/AotMinimal.csproj]</p><p>#12 6.230 /App/Program.cs(5,1): warning IL2026: Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder, String, Delegate)&apos; which has &apos;RequiresUnreferencedCodeAttribute&apos; can break functionality when trimming application code. This API may perform reflection on the supplied delegate and its parameters. These types may be trimmed if not directly referenced. [/App/AotMinimal.csproj]</p><p>#12 6.230 /App/Program.cs(5,1): warning IL3050: Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder, String, Delegate)&apos; which has &apos;RequiresDynamicCodeAttribute&apos; can break functionality when AOT compiling. This API may perform reflection on the supplied delegate and its parameters. These types may require generated code and aren&apos;t compatible with native AOT applications. [/App/AotMinimal.csproj]</p><p>#12 6.230 /App/Program.cs(5,1): warning IL2026: Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder, String, Delegate)&apos; which has &apos;RequiresUnreferencedCodeAttribute&apos; can break functionality when trimming application code. This API may perform reflection on the supplied delegate and its parameters. These types may be trimmed if not directly referenced. [/App/AotMinimal.csproj]</p><p>#12 6.713 AotMinimal -&gt; /App/bin/Release/net8.0/linux-x64/AotMinimal.dll</p><p>#12 7.174 Generating native code</p><p>#12 7.303 /App/Program.cs(5): Trim analysis warning IL2026: Program.&lt;Main&gt;$(String[]): Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder,String,Delegate)&apos; which has &apos;RequiresUnreferencedCodeAttribute&apos; can break functionality when trimming application code. This API may perform reflection on the supplied delegate and its parameters. These types may be trimmed if not directly referenced. [/App/AotMinimal.csproj]</p><p>#12 7.303 /App/Program.cs(5): AOT analysis warning IL3050: Program.&lt;Main&gt;$(String[]): Using member &apos;Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGet(IEndpointRouteBuilder,String,Delegate)&apos; which has &apos;RequiresDynamicCodeAttribute&apos; can break functionality when AOT compiling. This API may perform reflection on the supplied delegate and its parameters. These types may require generated code and aren&apos;t compatible with native AOT applications. [/App/AotMinimal.csproj]</p><p>#12 8.093 /root/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/8.0.0-preview.5.23280.8/framework/System.Linq.Expressions.dll : warning IL3053: Assembly &apos;System.Linq.Expressions&apos; produced AOT analysis warnings. [/App/AotMinimal.csproj]</p></div></div><p>The key message here is <code>These types may require generated code and aren&apos;t compatible with native AOT applications</code>. It turns out that JSON serialisation uses <em>source generation</em>, which is a thing that Native AOT does not support. </p><p>The <code>/</code> endpoint doesn&apos;t use source generation. If you omit the <code>/object</code> endpoint, it builds without warnings and runs fine. This is likely because the <code>EndpointRoutingMiddleware</code>, which is throwing that exception, needs to figure out all the routes, and just can&apos;t.</p><p>As mentioned earlier, <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=net7&amp;ref=arunstephens.com#limitations-of-native-aot-deployment">there&apos;s a significant list of things that can&apos;t be done with .NET AOT</a>. Key ones include:</p><ul><li>You can&apos;t do runtime code generation, which is the problem with the JSON serialisation here</li><li>You use dynamic loading, like using <code>Assembly.LoadFile</code></li><li>LINQ expressions always use the slower interpreted form</li></ul><p>Some new performance optimisations, like on-stack replacement, which I wrote about in February, also rely on the JIT nature of the runtime, where the runtime figures out a better way to run your code. If your code is already in machine code, it&apos;s not going to be able to be optimised on the fly. These features are also not able to be used with AOT.</p><h2 id="so-whats-next">So, what&apos;s next?</h2><p>Waiting.</p><p>Current ASP.NET APIs can&apos;t really take advantage of .NET Native AOT. You could limit yourself to using language and framework features that do work with it, if you need the performance improvements that this new technology brings.</p><p>For most workloads, though, I think you&apos;re better of sticking with what works well now. But I&apos;ll definitely be keeping an eye out on what you&apos;ll be able to do with AOT in the future.</p>]]></content:encoded></item><item><title><![CDATA[Cheating at work with ChatGPT]]></title><description><![CDATA[<p>My first recorded chat with ChatGPT was on 9 December 2022. I asked it to &quot;Write a prequel to Humpty Dumpty&quot;. It was pretty good, telling me that:</p><blockquote>Humpty Dumpty had a great desire to see the world beyond the castle walls, but he knew that his round,</blockquote>]]></description><link>https://arunstephens.com/cheating-at-work-with-chatgpt/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c771</guid><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 01 May 2023 02:15:21 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1675865254433-6ba341f0f00b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIyfHxjaGF0Z3B0fGVufDB8fHx8MTY4MjM1Njc2NA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1675865254433-6ba341f0f00b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIyfHxjaGF0Z3B0fGVufDB8fHx8MTY4MjM1Njc2NA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Cheating at work with ChatGPT"><p>My first recorded chat with ChatGPT was on 9 December 2022. I asked it to &quot;Write a prequel to Humpty Dumpty&quot;. It was pretty good, telling me that:</p><blockquote>Humpty Dumpty had a great desire to see the world beyond the castle walls, but he knew that his round, egg-like shape made it impossible for him to venture out on his own. So he spent his days dreaming of far-off lands and adventures...</blockquote><p>This one wasn&apos;t particularly useful for me at work, but not long later, I was clearly having a bit of trouble with PowerShell, and asked it &quot;unset env var in powershell&quot;. Sure enough, it gave me the answer. In probably about the same amount of time as a Google search would have.</p><p>I got a bit more ambitious. I had just had my work machine rebuilt, and was getting frustrated with IIS sites shutting down in the middle of the day, so I wanted to increase the idle timeout. I found some instructions on how to do it using the UI, but I have a lot of sites running, so I pasted in those instructions with the prefix &quot;Write me a powershell script to achieve the following:&quot; and sure enough, it gave me what I <em>almost </em>wanted.</p><p>And that&apos;s the trick with ChatGPT. You need to take its answers with a grain of salt. I asked ChatGPT to <a href="https://www.linkedin.com/posts/arunstephens_happy-new-year-everyone-as-we-start-the-activity-7018126326599483392-jFw1?utm_source=share&amp;utm_medium=member_desktop">write me a LinkedIn post about this very thing</a>. I think that, at the moment at least, generative AI tools like ChatGPT are great at helping you if you already know what you&apos;re doing. Code examples don&apos;t often work first time, and you need a bit of experience to troubleshoot the problem. That&apos;s not to say, though, that the technology won&apos;t improve. </p><p>If I&apos;m stuck and think my AI friend can help, my workflow depends on the problem. When what I&apos;m asking about isn&apos;t so specific to a problem with existing code, I&apos;ll just ask a question. Other times I might think giving ChatGPT the code I&apos;m having trouble with will help. If that&apos;s the case, I do my best to anonymise it, so I&apos;m not breaching any confidentiality obligations I have. A big concern companies have is that these AIs can then use that information as part of their training, and your confidential material may well end up in a future response to someone else&apos;s prompt.</p><p>It&apos;s still early days with tools like ChatGPT. As services start to have commercial terms that are more appealing to businesses, and users&apos; inputs are ringfenced so they don&apos;t end up being returned other users, the need to anonymise inputs will be a thing of the past.</p><p>But for now, I will often try to pare it back to just the thing that&apos;s giving me problems, much the same was as you might do when asking on Stack Overflow, or writing a blog post. This is where your own skill and experience becomes important. You need to know enough about your code in order to form a good prompt around it. </p><p>It&apos;s fairly well known that ChatGPT is not always right. Sometimes this is because it will just make things up (although arguably everthing it says it makes up), and other times it might be because its training data is misleading it. Knowing that it&apos;s wrong is another aspect of ChatGPT usage that relies on your own expertise.</p><p>Sometimes when I&apos;m reading a response, it will be clear to me that something is wrong. In these cases, I&apos;ll just say, for example, &quot;method doSomething does not exist on class Foo&quot; and it will invariably reply with an apology for the mistake, and a new solution. Maybe that solution will also have a problem, so I&apos;ll point out the mistake again. I might also help it by telling it what the solution it - &quot;doSomething doesn&apos;t exist but doThisThing does&quot; - and it might save us a bit of back and forth in coming up with the answer.</p><p>There might be times where I can&apos;t see the mistake. In these cases, I can just paste the error (or a description of it) that I get when it compiles or runs, and it will do the same thing. </p><p>My job is not just about writing code. I&apos;ve used it to help me formulate arguments for and against certain plans. I have given it a user story (which I anonymised first) and got it to give me a set of acceptance criteria. It&apos;s quite good at these. For non-work purposes, I&apos;ve pasted a whole lot of text in, and asked for it to rewrite it to read better. </p><p>I&apos;m really looking forward to the day that I can safely input private company data. I could send it a meeting transcript and get it to give me a summary of the key points. It could take all our Slack conversations and give me answers to questions about our sprint. </p><p>ChatGPT isn&apos;t a threat to my job yet. But it can definitely be a very useful tool. </p><p>What do you think? Have you used ChatGPT and friends in your day-to-day work? Let me know in the comments.</p>]]></content:encoded></item><item><title><![CDATA[Switch expressions in C#]]></title><description><![CDATA[<p>A recent introduction to the C# language is <em>pattern matching</em>. It was added in C# 7.0, which came out in March 2017, along with Visual Studio 2017 and .NET Framework 4.7. So it&apos;s a pretty well-established concept. But one I haven&apos;t used very much</p>]]></description><link>https://arunstephens.com/switch-expression/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c770</guid><category><![CDATA[Newsletter]]></category><category><![CDATA[Technology]]></category><category><![CDATA[.NET]]></category><category><![CDATA[Deep Dive]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Fri, 21 Apr 2023 20:29:31 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1623707430101-9e74cefe05e2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDV8fHN3aXRjaHxlbnwwfHx8fDE2ODE5MzQzMjc&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1623707430101-9e74cefe05e2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDV8fHN3aXRjaHxlbnwwfHx8fDE2ODE5MzQzMjc&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Switch expressions in C#"><p>A recent introduction to the C# language is <em>pattern matching</em>. It was added in C# 7.0, which came out in March 2017, along with Visual Studio 2017 and .NET Framework 4.7. So it&apos;s a pretty well-established concept. But one I haven&apos;t used very much at all.</p><p>The feature I want to write about today was introduced with C# 8.0, in September 2019 (around the time that .NET Core 3.0 came out). It is pattern matching using <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression?ref=arunstephens.com"><em>switch expressions</em></a>.</p><p>A quick intro if you&apos;re not a coder. A lot of programming is about making decisions. If something is true, do this, otherwise, do that. Sometimes it&apos;s not a binary choice. You might want to do different things depending on 3 or 4 or 10 different cases. This is where <code>switch</code> comes in. You specify each condition you&apos;re testing for, and what should happen if that condition is true. The usual way is a switch <em>statement</em> but there&apos;s a new way of achieving a similar thing, calls a switch <em>expression</em>.</p><p>I recently found an occasion to a switch expression in a recent piece of work. I am working on the API that serves a business web application. There is a backend API that could return an error as part of its regular operation, and that error is of an error type enum.</p><p>My API needs to return that error in the way the frontend will understand. I need to translate it.</p><!--kg-card-begin: markdown--><p>This is what comes from the backend API:</p>
<pre><code class="language-csharp">enum ErrorType
{
    Good,
    Bad,
    TooBig
}

class BackendApiResult
{
    ErrorType ErrorType { get; set; }
    string Message { get; set; }
}
</code></pre>
<p>My API&apos;s response needs to look like this:</p>
<pre><code class="language-js">{ 
    &quot;result&quot;: &quot;GOOD&quot; | &quot;BAD&quot; | &quot;TOO_BIG&quot;,
    &quot;maxSize&quot;: 1000 // only present if result is TOO_BIG
}
</code></pre>
<p>One way to do this would be:</p>
<pre><code class="language-csharp">switch (apiResponse.ErrorType)
{
    case ErrorType.Good:
        return new { Result = &quot;GOOD&quot; };

    case ErrorType.Bad:
        return new { Result = &quot;BAD&quot; };

    case ErrorType.TooBig:
        return new { Result = &quot;TOO_BIG&quot;, MaxSize = GetMaxSize(apiResponse) };

    default:
        return new { Result = apiResponse.Message };
}
</code></pre>
<p>It&apos;s quite verbose. But to use pattern matching:</p>
<pre><code class="language-csharp">return apiResponse.ErrorType switch
{
    ErrorType.Good =&gt; new { Result = &quot;GOOD&quot; },
    ErrorType.Bad =&gt; new { Result = &quot;BAD&quot; },
    ErrorType.TooBig =&gt; new { Result = &quot;TOO_BIG&quot;, MaxSize = GetMaxSize(apiResponse) },
    _ =&gt; new { Result = apiResponse.Message },
};
</code></pre>
<p>Less wordy, possibly easier to follow, assuming you know the new syntax.</p>
<p>It should be mostly self-explanatory. The <code>_</code> is the <em>discard pattern</em>, a catch-all, similar to the <code>default</code> in a <code>switch</code> statement.</p>
<p>What is different from a <code>switch</code> statement is that the compiler will warn you if you don&apos;t cover all the cases. If I hadn&apos;t included the discard pattern at the end, I&apos;d have received</p>
<blockquote>
<p>Warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern &apos;(ErrorType)3&apos; is not covered.</p>
</blockquote>
<p>The compiler knows that other values could be provided in the ErrorType property, and lets me know. To be fair, in this example, if I didn&apos;t have the default case, the compiler would tell me that not all code paths return a value, but it&apos;s not as explicit as to what was missing.</p>
<p>What does it look like behind the scenes? I loaded the code into dotPeek, and looked at the low-level C# view of the IL. They are very similar.</p>
<p>The switch statement implementation looks like this:</p>
<pre><code class="language-csharp">[NullableContext(1)]
[CompilerGenerated]
internal static object \u003C\u003CMain\u003E\u0024\u003Eg__GetThingTheOldWay\u007C0_1(
  BackendApiResult apiResponse)
  {
    switch (apiResponse.ErrorType)
    {
      case ErrorType.Good:
        return (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(&quot;GOOD&quot;);
      case ErrorType.Bad:
        return (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(&quot;BAD&quot;);
      case ErrorType.TooBig:
        return (object) new \u003C\u003Ef__AnonymousType1&lt;string, int&gt;(&quot;TOO_BIG&quot;, Program.\u003C\u003CMain\u003E\u0024\u003Eg__GetMaxSize\u007C0_2(apiResponse));
      default:
        return (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(apiResponse.Message);
    }
}
</code></pre>
<p>The pattern matching implementation looks like:</p>
<pre><code class="language-csharp">[NullableContext(1)]
[CompilerGenerated]
internal static object u003C\u003CMain\u003E\u0024\u003Eg__GetThing\u007C0_0(
  BackendApiResult apiResponse)
{
  ErrorType errorType = apiResponse.ErrorType;
  if (true)
    ;
  object thing00;
  switch (errorType)
  {
    case ErrorType.Good:
      thing00 = (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(&quot;GOOD&quot;);
      break;
    case ErrorType.Bad:
      thing00 = (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(&quot;BAD&quot;);
      break;
    case ErrorType.TooBig:
      thing00 = (object) new \u003C\u003Ef__AnonymousType1&lt;string, int&gt;(&quot;TOO_BIG&quot;, Program.\u003C\u003CMain\u003E\u0024\u003Eg__GetMaxSize\u007C0_2(apiResponse));
      break;
    default:
      thing00 = (object) new \u003C\u003Ef__AnonymousType0&lt;string&gt;(apiResponse.Message);
      break;
  }
  if (true)
    ;
  return thing00;
}
</code></pre>
<p>Apart from assigning the property to a variable at the start (which in some languages is more efficient because the expression will be evaluated for each case - but this isn&apos;t how it works in .NET) and saving the result in a variable and returning it at the end, they are the same.</p>
<p>Which is faster? My hypothesis is that the one that the switch statement will be. After all, it&apos;s doing less work.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://arunstephens.com/content/images/2023/04/switch-expression-vs-statement.png" class="kg-image" alt="Switch expressions in C#" loading="lazy" width="2000" height="1306" srcset="https://arunstephens.com/content/images/size/w600/2023/04/switch-expression-vs-statement.png 600w, https://arunstephens.com/content/images/size/w1000/2023/04/switch-expression-vs-statement.png 1000w, https://arunstephens.com/content/images/size/w1600/2023/04/switch-expression-vs-statement.png 1600w, https://arunstephens.com/content/images/size/w2400/2023/04/switch-expression-vs-statement.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Box and whisker of time taken for switch expression vs switch statement</figcaption></figure><p>Looking at the graph above, you can see it&apos;s slightly faster to use the switch statement. My test involved calling a switch statement/expression 100,000,000 times. I ran it 1,000 times for the expression and 1,000 for the statement and recorded the time tacken (in ticks). It test wasn&apos;t very scientific and the computer was doing other things while it was running!</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Method</th>
<th>Average time</th>
<th>Standard deviation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Expression</td>
<td>4071</td>
<td>1149</td>
</tr>
<tr>
<td>Statement</td>
<td>3870</td>
<td>1138</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>So the statement appears to be about 5% faster. Again, the test isn&apos;t that scientific so I wouldn&apos;t base any decision on whether to use an expression or statement on this alone!</p><p>What do you think? Do you use switch expressions? Let me know in the comments!</p>]]></content:encoded></item><item><title><![CDATA[A new(sletter) journey]]></title><description><![CDATA[<p>Hi!</p><p>I&apos;ve decided to give the blog a revamp, and actually relaunch it as a newsletter. I&apos;ll be writing about software development and general technology things that interest me and that I think might interest you.</p><p>Why a newsletter? A few reasons. First, newsletters are cool</p>]]></description><link>https://arunstephens.com/a-newsletter-journey/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c76f</guid><category><![CDATA[Newsletter]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Wed, 19 Apr 2023 09:07:23 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1530819568329-97653eafbbfa?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDh8fFR5cGV3cml0ZXIlMjB8ZW58MHx8fHwxNjgxODk0ODY4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1530819568329-97653eafbbfa?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDh8fFR5cGV3cml0ZXIlMjB8ZW58MHx8fHwxNjgxODk0ODY4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="A new(sletter) journey"><p>Hi!</p><p>I&apos;ve decided to give the blog a revamp, and actually relaunch it as a newsletter. I&apos;ll be writing about software development and general technology things that interest me and that I think might interest you.</p><p>Why a newsletter? A few reasons. First, newsletters are cool these days. Much cooler than blogs. They also allow you to have a bit more control over how your content gets to people. Which pretty much means you aren&apos;t at the mercy of Zuck&apos;s algorithms or Elon&apos;s whims. </p><p>I also want to write more. I was thinking of topics to write about and I realised I&apos;ve been in this game for a long time now, and have some good stories to tell. Having a newsletter, and eventually having people who I know are going to at least have what I&apos;ve written end up in their inboxes, will (hopefully) encourage me to write more frequently.</p><p>From a professional point of view, I&apos;d like to get better at technical (and non-technical) writing. As an engineering leader, a big part of my job is to explain technical things to people from all around the business. Practice makes perfect, so thank you, guinea pigs!</p><p>And of course, there&apos;s a dream that one day I might feel like I can become a thought leader that can charge for my most premium thoughts. That day is a long way off, if it ever comes at all.</p><p>Everything that used to be on my blog is still here, including when it was less focussed and whatever came from my mind. I might still write like that from time to time.</p><p>So, if this sounds like something you might vaguely want to read, please subscribe!</p><p>Arun</p>]]></content:encoded></item><item><title><![CDATA[What's new in .NET 7]]></title><description><![CDATA[.NET 7 was released a few months ago, and it brought many exciting new features. As a .NET developer, I am particularly interested in the changes that affect my work . In this article, I will go through some of the new features in .NET 7 that I think are worth highlighting.]]></description><link>https://arunstephens.com/whats-new-in-net-7/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c76e</guid><category><![CDATA[Newsletter]]></category><category><![CDATA[.NET]]></category><category><![CDATA[Technology]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Thu, 16 Feb 2023 01:30:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1598182734765-0d97fb755ed0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIxfHxOZXR8ZW58MHx8fHwxNjgxODk2MDA3&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1598182734765-0d97fb755ed0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDIxfHxOZXR8ZW58MHx8fHwxNjgxODk2MDA3&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="What&apos;s new in .NET 7"><p>.NET 7 was released a few months ago, and it brought many exciting new features. As a .NET developer, I am particularly interested in the changes that affect my <a href="https://jobs.lever.co/xero?lever-via=nmJK_Gngo8&amp;ref=arunstephens.com">work</a> . In this article, I will go through some of the new features in .NET 7 that I think are worth highlighting.</p><p>The features I want to go through are in the following areas:</p><ul><li>Performance</li><li>C# 11</li><li>JSON serialisation/deserialisation</li><li>Rate limiting</li><li>Output caching</li></ul><p>You can read more detail about all of the new features on <a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-7?ref=arunstephens.com">Microsoft&#x2019;s detailed what&#x2019;s new page</a>, which was my starting point for this article.</p><h3 id="runtime-performance-improvements">Runtime performance improvements</h3><p>One of the main focuses of .NET 7 was performance. This has the potential to give your users a better experience, improving load times. It can also potentially mean lower running costs for your business, as you&#x2019;ll be able to squeeze a bit more out of the compute resource you have allocated. And you should be able to get this benefit with very little development effort!</p><p>If the runtime figures out a faster way to run your code, while your application is running, <strong>on-stack replacement</strong> (OSR) lets it switch to that faster way. OSR is used when a method has long-running loops. So, after a few iterations, the runtime might find a better way to do the next ones. It will use that better way on the following iteration. It is turned on by default.</p><p>Related to OSR is a feature you need to turn on to take advantage of. <strong>Dynamic profile-guided optimisation</strong> is the way that the runtime decides if there&#x2019;s a better way to run your code. It collects data on how your application is running, and uses that to optimise future executions. One benchmark saw a <a href="https://petabridge.com/blog/dotnet7-pgo-performance-improvements/?ref=arunstephens.com">33% improvement in execution speed</a> with dynamic PGO turned on.</p><p>I don&#x2019;t think there is a way for the runtime to keep track of its more optimised ways of running your code if you need to restart your application. But even if it did, when running in Kubernetes, when spinning up new pods, it will never have run before.</p><h3 id="c-11">C# 11</h3><p>There are a few changes to C#, but one that I am glad is finally here is <strong>raw string literals</strong>, Python-style. You can now do this!</p><pre><code>var myString = &quot;&quot;&quot;
    This is a string that has \ in it and also &quot; and also &apos; and
    it can go on multiple lines. Also this 
    &lt;--- whitespace gets stripped because it is the amount 
    of whitespace to the left of the quotes on the 
    final line
    &quot;&quot;&quot;;
</code></pre><p>The indent is removed automatically, and the size of the indent is determined by the indent of the closing <code>&quot;&quot;&quot;</code> of the string. If any line in your literal has an indent less than that one, the compiler will give you an error.</p><p>You can also combine it with string interpolation. The number of <code>$</code> you put to open your literal is the number of <code>{</code> you need to surround your expression with, e.g.:</p><pre><code>var myString = $$&quot;&quot;&quot;
    This needs another brace around {DateTime.UtcNow} to put the date in, see: {{DateTime.UtcNow}}.
    &quot;&quot;&quot;;
</code></pre><p>Value of <code>myString</code>:</p><pre><code>This needs another brace around {DateTime.UtcNow} to put the date in, see: 15/02/2023 20:47:10.
</code></pre><p>There&#x2019;s also a new <strong>required modifier</strong> for fields and properties on classes. This tells the compiler that the object initialiser must set that field or property. If you don&#x2019;t, there will be a compiler error.</p><p>If you have this class:</p><pre><code>public class HasRequired
{
    public required string Name { get; set; }

    public HasRequired()
    {
        // I don&apos;t set the name
    }
}
</code></pre><p>Then this initialiser:</p><pre><code>var obj = new HasRequired();
</code></pre><p>will not compile because it needs to set the <code>Name</code> property, like this:</p><pre><code>var obj = new HasRequired { Name = &quot;my name&quot; };
</code></pre><p>If you had a constructor:</p><pre><code>public HasRequired(string name)
{
    Name = name;
}
</code></pre><p>Then you would still get the same compiler error. You can prevent this by using the <code>[SetsRequiredMembers]</code> attribute on the constructor. You are telling the compiler that if the consumer uses this constructor, all required members will be set. The <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required?ref=arunstephens.com">docs say &#x201C;Use it with caution&#x201D;</a>.</p><h3 id="json-serialisation-and-deserialisation">JSON serialisation and deserialisation</h3><p>System.Text.Json is acknowledged to be more performant than what was effectively the de facto industry standard, <a href="https://www.newtonsoft.com/json?ref=arunstephens.com">Newtonsoft.Json</a>. But it does lack some features. This is beginning to change.</p><p>There is now <strong><a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/custom-contracts?ref=arunstephens.com">contract customisation</a></strong> which lets you write functions that change how the serialisation process works. Using this feature, you can distinguish between <code>null</code> and not-present values in a JSON payload. I can see this being useful if you, for example, define your API to not update a field if it&#x2019;s not present in the payload, and setting it to null if it&#x2019;s set to null in the payload. You can also use it to serialise private fields and properties (you would need to use reflection to get the values, though) or ignore properties by name, value or type.</p><p>If you are using inheritance in your data model (and there are strong opinions that you shouldn&#x2019;t in product development; I haven&#x2019;t formed my own on that yet) then this next one could be useful. You can now <strong><a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0&amp;ref=arunstephens.com">serialise properties of derived classes</a></strong>. This is something that Newtonsoft did out of the box, and that System.Text.Json decided not to do to prevent you from accidentally sending private data over the wire.</p><p>Take this model:</p><pre><code>public class Bar
{
    public string BarValue { get; set; }
}

public class Foo : Bar
{
    public string FooValue { get; set; }
}
</code></pre><p>In .NET 6, if you were to serialise this:</p><pre><code>Bar foo = new Foo { FooValue = &quot;foo&quot;, BarValue = &quot;bar&quot; }
</code></pre><p>You would end up with:</p><pre><code>{ &quot;barValue&quot;: &quot;bar&quot; }
</code></pre><p>You might have expected:</p><pre><code>{ &quot;barValue&quot;: &quot;bar&quot;, &quot;fooValue&quot;: &quot;foo&quot; }
</code></pre><p>This feature was there to prevent you from accidentally exposing something that&#x2019;s public in your code, but private in terms of whether it should be sent down the wire.</p><p>You can now explicitly declare that <code>Foo</code> is a derived type of <code>Bar</code>, and the serialiser will know that you want to include all of <code>Foo</code> if one is passed as <code>Bar</code>:</p><pre><code>[JsonDerivedType(typeof(Foo))]
public class Bar
{   
    ...
}
</code></pre><h3 id="rate-limiting">Rate limiting</h3><p>There is a new System.Threading.RateLimiting package as part of .NET. There is also <strong>rate limiting middleware</strong> built into ASP.NET Core. There are several rate limiting methods available:</p><ul><li>Fixed window</li><li>Sliding window</li><li>Token bucket</li><li>Concurrency</li></ul><p>They are very easy to set up.</p><p>The <a href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&amp;view=aspnetcore-7.0&amp;ref=arunstephens.com">documentation</a> gives very good examples on how they work, so I won&#x2019;t repeat them here. It is also very explicit that you should perform load testing, and gives some examples of using <a href="https://jmeter.apache.org/?ref=arunstephens.com">JMeter</a>. Another common load testing tool is <a href="https://k6.io/?ref=arunstephens.com">k6</a>.</p><p>This is probably my favourite feature, as someone who builds scalable systems.</p><h3 id="output-caching-middleware">Output caching middleware</h3><p>You can now <a href="https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-7.0&amp;ref=arunstephens.com">cache the output</a> of your app. These days I&#x2019;m more concerned with API controllers, but it works for all types of ASP.NET Core apps: minimal API, MVC, and Razor pages as well. Once you&#x2019;ve added the middleware to your application, if you simply add a <code>[OutputCache]</code> attribute to your action method, the system will cache the entire output of your response. The default cache key uses the whole URL, but you can customise it.</p><p>You can also evict cache entries. This is done by tag. When adding the <code>[OutputCache]</code> attribute, you can group methods together by tag, and then cache eviction will affect all methods with that tag.</p><p>It&#x2019;s worth noting that this works regardless of what the client sends down in its <code>Cache-Control</code> header, in contrast to <a href="https://learn.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-7.0&amp;ref=arunstephens.com">response caching</a>, which was already part of .NET 6.</p><h3 id="summary">Summary</h3><p>This is just a small taste of the new features available in .NET 7. Bear in mind that .NET 7 is a standard term support release, so if you move to it, you&#x2019;ll need to upgrade to .NET 8 by May 2024. .NET 6 has support until November 2024. But these features may be worth the effort of doing two upgrades over the next year and a bit.</p>]]></content:encoded></item><item><title><![CDATA[What's inside My Vaccine Pass? (Part 1)]]></title><description><![CDATA[When they were talking about how vaccine passports might work, I had an idea of how I’d make such a system. A QR code with some information about each person and their vaccination status, a signature signed with the private key of the issuer, and a way for people to verify that signature with the issuer’s public key.]]></description><link>https://arunstephens.com/whats-inside-my-vaccine-pass-part-1/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c768</guid><category><![CDATA[Newsletter]]></category><category><![CDATA[Deep Dive]]></category><category><![CDATA[Technology]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Mon, 06 Dec 2021 01:27:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1640900077170-a832df4330ed?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fFZhY2NpbmUlMjBwYXNzfGVufDB8fHx8MTY4MTg5NjA3Ng&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1640900077170-a832df4330ed?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fFZhY2NpbmUlMjBwYXNzfGVufDB8fHx8MTY4MTg5NjA3Ng&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="What&apos;s inside My Vaccine Pass? (Part 1)"><p>When they were talking about how vaccine passports might work, I had an idea of how I&#x2019;d make such a system. A QR code with some information about each person and their vaccination status, a signature signed with the private key of the issuer, and a way for people to verify that signature with the issuer&#x2019;s public key.</p><p>It turns out that it&#x2019;s exactly what they&#x2019;ve done. And it&#x2019;s very similar to what happens every day on the web with OAuth2 and JWT.</p><p>I wanted to decode and verify my COVID pass so I can see how it works. So that&#x2019;s what I did.</p><p>The spec for the system is public, at <a href="https://nzcp.covid19.health.nz/?ref=arunstephens.com">https://nzcp.covid19.health.nz/</a>. (It&#x2019;s actually a GitHub Pages site.)</p><p>First up, I have to decode my QR code:</p><pre><code>NZCP:/1/2KCE3I...KLCMJQ
</code></pre><p>(Most of it omitted for what should be obvious reasons.)</p><p>The first thing the spec says is:</p><blockquote>Check if the message received from the QR Code begins with the prefix NZCP:/, if it does not then fail.</blockquote><p>Yes, it is. It starts with <code>NZCP:/</code></p><p>Next:</p><blockquote>Parse the character(s) (representing the <code>version-identifier</code>) as an unsigned integer following the <code>NZCP:/</code> suffix and before the next slash character (<code>/</code>) encountered. If this errors then fail. If the value returned is un-recognized as a major protocol version supported by the verifying software then fail. NOTE - for instance in this version of the specification this value MUST be 1.</blockquote><p>Tick. It is <code>NZCP:/1/</code>.</p><blockquote>With the remainder of the message following the <code>/</code> after the <code>version-identifier</code>, attempt to decode it using base32 as defined by <a href="https://nzcp.covid19.health.nz/?ref=arunstephens.com#ref:RFC4648">RFC4648</a> <strong>NOTE</strong> add back in padding if required, if an error is encountered during decoding then fail.</blockquote><p>Now things get a bit trickier, as I can&#x2019;t decode base32 in my head. So I&#x2019;ll need to write some code. Base32 isn&#x2019;t part of the .NET standard library either. So I <a href="https://github.com/ssg/SimpleBase?ref=arunstephens.com">found something on NuGet</a> that had lots of downloads. Maybe it will work.</p><pre><code class="language-csharp">var decodedBytes = SimpleBase.Base32.Rfc4648.Decode(messageBase32);
</code></pre><p>I appear to have an array of bytes. (Actually it&#x2019;s a <code>Span&lt;byte&gt;</code>.)</p><p>Next step:</p><blockquote>With the decoded message attempt to decode it as <code>COSE_Sign1</code> CBOR structure, if an error is encountered during decoding then fail.</blockquote><p>CBOR stands for Concise Binary Object Representation and it is essentially a binary version of JSON. The System.Formats.Cbor package can decode them.</p><pre><code class="language-csharp">var message = new CborReader(decodedBytes);
</code></pre><p>I&#x2019;m not sure if this is a <code>COSE_Sign1</code> structure though. <a href="https://datatracker.ietf.org/doc/html/rfc8152?ref=arunstephens.com">RFC8152</a> tells me that COSE means CBOR Object Signing and Encryption. CBOR Tag 18 is the one for <code>COSE_Sign1</code>, and it appears that my message has that tag.</p><p>Now:</p><blockquote>With the headers returned from the <code>COSE_Sign1</code> decoding step, check for the presence of the required headers as defined in the data model section, if these conditions are not meet then fail.</blockquote><p>The message itself is an array. The first element is the header:</p><pre><code class="language-csharp">var arrayLength = message.ReadStartArray();
var headerBytes = message.ReadByteString();
var headerMessage = new CborReader(headerBytes);
</code></pre><p>This code decodes the header.</p><pre><code class="language-csharp">var headerLength = headerMessage.ReadStartMap();

int algValue = 0;
string kidValue = null;

for (var i = 0; i &lt; headerLength; i++)
{
    var headerKey = headerMessage.ReadInt32();

    if (headerKey == 1)
    {
        // it&apos;s alg
        algValue = headerMessage.ReadInt32();
    }
    else if (headerKey == 4)
    {
        // it&apos;s kid
        var headerValueBytes = headerMessage.ReadByteString();
        kidValue = Encoding.ASCII.GetString(headerValueBytes);
    }
}

headerMessage.ReadEndMap();
</code></pre><p>It is a map with two key-value pairs. COSE has some <a href="https://www.iana.org/assignments/cose/cose.xhtml?ref=arunstephens.com">well-known keys with IDs</a>. We have keys here identified by <code>1</code> and <code>4</code> which represent <code>alg</code> and <code>kid</code> respectively.</p><p>The value of the <code>alg</code> is a single byte: <code>-7</code>. This represents the algorithm ES256. The value of <code>kid</code> is a byte string, which I&#x2019;ve decoded as ASCII:</p><pre><code class="language-csharp">WriteLine($&quot;Header decoded. Alg = {algValue}, Kid = {kidValue}&quot;);
</code></pre><p>Output:</p><pre><code>Header decoded. Alg = -7, Kid = z12Kf7UQ
</code></pre><p>I&#x2019;m not sure if that&#x2019;s a valid key ID yet. We will check that later.</p><p>So far, we&#x2019;ve got a valid <code>alg</code> and <code>kid</code>. There are other headers in the rest of the message that we still need to validate.</p><p>The second element of <code>message</code> is an empty map. Skip it!</p><pre><code class="language-csharp">var mapLength = message.ReadStartMap();
message.ReadEndMap();
</code></pre><p>The third element contains the actual payload, as a byte string:</p><pre><code class="language-csharp">var payloadBytes = message.ReadByteString();
var payloadMessage = new CborReader(payloadBytes);
var payloadLength = payloadMessage.ReadStartMap();
</code></pre><p>There are some well-known keys, identified by an integer, and keys can also be strings. We have both in our payload. The integer keys are the standard headers. The final element is <code>@vc</code> which is our actual COVID Pass.</p><pre><code class="language-csharp">for (var i = 0; i &lt; payloadLength; i++)
{
    var nextOne = payloadMessage.PeekState();

    if (nextOne == CborReaderState.TextString)
    {
        var keyString = payloadMessage.ReadTextString();
        if (keyString.ToString() == &quot;vc&quot;)
        {
            var vcLength = payloadMessage.ReadStartMap();

            for (var j = 0; j &lt; vcLength; j++)
            {
                var key = payloadMessage.ReadTextString();

                switch (key)
                {
                    case &quot;@context&quot;:

                        pass.Context = ReadStringArray(payloadMessage);

                        break;

                    case &quot;version&quot;:
                        pass.Version = payloadMessage.ReadTextString();
                        break;

                    case &quot;type&quot;:
                        pass.Type= ReadStringArray(payloadMessage);
                        break;

                    case &quot;credentialSubject&quot;:
                        pass.CredentialSubject = ReadCredentialSubject(payloadMessage);
                        break;

                    default:
                        break;
                }
            }

            payloadMessage.ReadEndMap();
        }
    }
    else
    {
        var payloadKey = payloadMessage.ReadInt32();

        switch (payloadKey)
        {
            case 1:
                issValue = payloadMessage.ReadTextString();
                break;

            case 4:
                nbfValue = payloadMessage.ReadInt32();
                break;

            case 5:
                expValue = payloadMessage.ReadInt32();
                break;

            case 7:
                jtiBytes = payloadMessage.ReadByteString();
                break;
        }
    }
}
</code></pre><p>At the end, we get this:</p><p>Key (int) Key (description) Value 1 <code>iss</code> (Issuer) <code>did:web:nzcp.identity.health.nz</code> 4 <code>exp</code> (Expiry date) <code>1652702400</code> (Monday, 16 May 2022 12:00:00 UTC) 5 <code>nbf</code> (Valid from) <code>1637060400</code> (Tuesday, 16 November 2021 11:00:00 UTC) 7 <code>jti</code> (Token identifier) ByteString, which is a UUID according to the spec (not shown) <code>vc</code> Verifiable credential object</p><p>The <code>vc</code> is an object containing all the claims:</p><p>Key (string) Description Value <code>@context</code> Array of strings with URLs <code>version</code> Version number <code>1.0.0</code> <code>type</code> Array of types <code>[ &quot;VerifiableCredential&quot;, &quot;PublicCovidPass&quot; ]</code> <code>credentialSubject</code> Map of name <code>{ &quot;givenName&quot;: &quot;ARUN&quot;, &quot;familyName&quot;: &quot;STEPHENS&quot;, &quot;dob&quot;: &quot;yyyy-MM-dd&quot; }</code></p><p>Of note is that your NHI number is not part of the payload, nor is there any information about your specific vaccine status. The only thing that matters to operate the traffic light system is that you have a pass - it could be because you are vaccinated or it may be because of an exemption. The pass doesn&#x2019;t care.</p><p>Also of note is that the <code>jti</code> claim is a UUID, but you can&#x2019;t just pass the bytes in that order into the constructor for a .NET <code>Guid</code> structure, because <code>Guid</code> expects a different byte order for some segments. To turn it into a UUID URN, I have this very ugly code:</p><pre><code class="language-csharp">var jtiUuid = new byte[][]
{
    jtiBytes.Skip(0).Take(4).ToArray(),
    jtiBytes.Skip(4).Take(2).ToArray(),
    jtiBytes.Skip(6).Take(2).ToArray(),
    jtiBytes.Skip(8).Take(2).ToArray(),
    jtiBytes.Skip(10).ToArray()
};

var jtiString = $&quot;urn:uuid:{Convert.ToHexString(jtiUuid[0])}-{Convert.ToHexString(jtiUuid[1])}-{Convert.ToHexString(jtiUuid[2])}-{Convert.ToHexString(jtiUuid[3])}-{Convert.ToHexString(jtiUuid[4])}&quot;.ToLower();
</code></pre><p>We now know everything the holder of this pass is claiming. But are their claims valid?</p><blockquote>Validate that the <code>iss</code> claim in the decoded CWT payload is an issuer you trust refer to the <a href="https://nzcp.covid19.health.nz/?ref=arunstephens.com#trusted-issuers">trusted issuers section</a> for a trusted list, if not then fail.</blockquote><p>The value was <code>did:web:nzcp.identity.health.nz</code> and we trust this issuer. Although I would argue that if it had a <code>.govt.nz</code> domain it would seem more trustworthy.</p><blockquote>Following the rules outlined in [issuer identifier(https://nzcp.covid19.health.nz/#issuer-identifier)] retrieve the issuers public key that was used to sign the CWT, if an error occurs then fail.</blockquote><p>We can retrieve the public key from the DID configuration at <a href="https://nzcp.identity.health.nz/.well-known/did.json?ref=arunstephens.com">https://nzcp.identity.health.nz/.well-known/did.json</a>.</p><pre><code class="language-json">{
    &quot;id&quot;: &quot;did:web:nzcp.identity.health.nz&quot;,
    &quot;@context&quot;: [
        &quot;https://w3.org/ns/did/v1&quot;,
        &quot;https://w3id.org/security/suites/jws-2020/v1&quot;
    ],
    &quot;verificationMethod&quot;: [
        {
            &quot;id&quot;: &quot;did:web:nzcp.identity.health.nz#z12Kf7UQ&quot;,
            &quot;controller&quot;: &quot;did:web:nzcp.identity.health.nz&quot;,
            &quot;type&quot;: &quot;JsonWebKey2020&quot;,
            &quot;publicKeyJwk&quot;: {
                &quot;kty&quot;: &quot;EC&quot;,
                &quot;crv&quot;: &quot;P-256&quot;,
                &quot;x&quot;: &quot;DQCKJusqMsT0u7CjpmhjVGkHln3A3fS-ayeH4Nu52tc&quot;,
                &quot;y&quot;: &quot;lxgWzsLtVI8fqZmTPPo9nZ-kzGs7w7XO8-rUU68OxmI&quot;
            }
        }
    ],
    &quot;assertionMethod&quot;: [
        &quot;did:web:nzcp.identity.health.nz#z12Kf7UQ&quot;
    ]
}
</code></pre><p>You will note that the key ID from above (<code>z12Kf7UQ</code>) is present in the DID document. So it must be a valid key.</p><p>We need to extract the JWK from there, and then validate the entire original message with that key.</p><blockquote>With the retrieved public key validate the digital signature over the <code>COSE_Sign1</code> structure, if an error occurs then fail.</blockquote><p>Up until now we have just been parsing a CBOR object. Now we have to validate the signature. Microsoft doesn&#x2019;t provide anything that is specific for COSE. So we will leave that for the next post, along with an explanation of how this is very similar to a lot of authentication that happens on the web.</p>]]></content:encoded></item><item><title><![CDATA[Why does Docker restart all containers regularly?]]></title><description><![CDATA[For as long as I can remember, every now and then, all my Docker containers restart. I had no idea why. I am running Ubuntu 16.04 LTS (Xenial). I got the exact start time of my containers like so: docker inspect --format='{{.State.StartedAt}}' <CONTAINERID>]]></description><link>https://arunstephens.com/why-does-docker-restart-all-containers-regularly/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c769</guid><category><![CDATA[Newsletter]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Troubleshooting]]></category><category><![CDATA[Technology]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Sat, 07 Oct 2017 13:28:02 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1605745341112-85968b19335b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fERvY2tlcnxlbnwwfHx8fDE2ODE4OTYxMzA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1605745341112-85968b19335b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fERvY2tlcnxlbnwwfHx8fDE2ODE4OTYxMzA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Why does Docker restart all containers regularly?"><p>For as long as I can remember, every now and then, all my Docker containers restart. I had no idea why.</p><p>I am running Ubuntu 16.04 LTS (Xenial).</p><p>I got the exact start time of my containers like so:</p><pre><code class="language-bash">docker inspect --format=&apos;{{.State.StartedAt}}&apos; &lt;CONTAINERID&gt;
</code></pre><p>(Thanks to <a href="http://timjrobinson.com/get-docker-container-start-time-in-unix-timestamp-seconds/?ref=arunstephens.com">Tim Robinson</a> for that bit of magic.)</p><p>Then I looked in the syslog for entries around that date. (I used <code>lnav</code> to make that a bit easier to do.)</p><p>This is what I found:</p><pre><code class="language-syslog">Oct  7 06:09:52 jandell systemd[1]: Starting Daily apt upgrade and clean activities...
Oct  7 06:10:04 jandell systemd[1]: Stopping Docker Application Container Engine...
</code></pre><p>It looks like <code>apt</code> does stuff every morning, and as part of it, if it deems it to be required, restarts Docker. This is likely because things Docker has been using have been upgraded. Sometimes when I install or upgrade, I&#x2019;ll get a full screen prompt asking which services to restart because dependent libraries have been updated. Docker is often one of the candidates.</p><p>So how do I stop this? Do I want to?</p><p>The main problem is that my nginx + docker-gen container don&#x2019;t properly restart after Docker does. If I&#x2019;m in charge upgrading Docker, then I can live with this and just make sure all my services are running after I do my upgrade. I don&#x2019;t want the system automatically upgrading itself.</p><p>Based on the <a href="https://gist.github.com/noromanba/6e062d38fd7fd2cd609a6ef1c26ea7bc?ref=arunstephens.com">Gist &#x201C;how to disable apt-daily.timer&#x201D;</a>, I&#x2019;ve determined I can disable the automatic upgrade task <code>apt-daily-upgrade.timer</code>:</p><pre><code class="language-bash">$ systemctl stop apt-daily-upgrade.timer
$ systemctl disable apt-daily-upgrade.timer
$ systemctl daemon-reload
</code></pre><p>This should hopefully stop <code>apt</code> from restarting things when I don&#x2019;t know about it.</p><p>As to why <code>nginx-gen</code> doesn&#x2019;t restart when it should, that&#x2019;s for another time.</p>]]></content:encoded></item><item><title><![CDATA[View a config file without comments]]></title><description><![CDATA[Sometimes you have a config file that has is heavily annotated (which is great). But sometimes you just want to see the actual configuration without all that help text. Use grep’s -v (invert) option: cat config | grep -v \# (Note that the # has to be escaped.)]]></description><link>https://arunstephens.com/view-a-config-file-without-comments/</link><guid isPermaLink="false">66a6cafccc4fac0142c9c76d</guid><category><![CDATA[Newsletter]]></category><category><![CDATA[Tips and Tricks]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Technology]]></category><dc:creator><![CDATA[Arun Stephens]]></dc:creator><pubDate>Sat, 25 Jun 2016 07:49:33 GMT</pubDate><content:encoded><![CDATA[<p>Sometimes you have a config file that has is heavily annotated (which is great). But sometimes you just want to see the actual configuration without all that help text.</p><p>Use grep&#x2019;s <code>-v</code> (invert) option:</p><pre><code>cat config | grep -v \#
</code></pre><p>(Note that the <code>#</code> has to be escaped.)</p>]]></content:encoded></item></channel></rss>