<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Victorio Berra</title>
    <description>The latest articles on Forem by Victorio Berra (@victorioberra).</description>
    <link>https://forem.com/victorioberra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F34940%2Fa01eff49-3ef8-412c-9092-752e492d994f.png</url>
      <title>Forem: Victorio Berra</title>
      <link>https://forem.com/victorioberra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/victorioberra"/>
    <language>en</language>
    <item>
      <title>Build an Aspire API Using Microsoft OpenAI, Scalar, OpenRouter, Structured Output, and Custom Headers</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Fri, 03 Oct 2025 18:34:51 +0000</pubDate>
      <link>https://forem.com/victorioberra/build-an-aspire-api-using-microsoft-openai-scalar-openrouter-structured-output-and-custom-3a6</link>
      <guid>https://forem.com/victorioberra/build-an-aspire-api-using-microsoft-openai-scalar-openrouter-structured-output-and-custom-3a6</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-10-04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"temperatureC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-10-04: Crisp 8°C with intermittent interdimensional drizzle — expect marmalade rain and the occasional polite wormhole. 40% chance of phosphorescent fog veins, 20% chance of localized anti-gravity gusts (hold onto your hat; it may orbit you), and a small probability of spontaneous teleporting puddles that prefer not to be stepped in. Bring an umbrella and a treaty offer — umbrellas may demand a cup of tea before cooperating."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"temperatureF"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bootstraping
&lt;/h2&gt;

&lt;p&gt;Create a new Aspire solution:&lt;/p&gt;

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

&lt;p&gt;Install &lt;code&gt;Microsoft.Extensions.AI.OpenAI&lt;/code&gt; &lt;strong&gt;pre-release&lt;/strong&gt; in your ApiService project. Install non-prerelease &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt; as well. Things like structured output extensions are in &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;Scalar.Aspire&lt;/code&gt; in your AppHost project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scalar Aspire
&lt;/h2&gt;

&lt;p&gt;Microsoft is moving away from including Swashbuckle UI stuff in their templates and docs. They now have their own OpenApi (not to be confused with OpenAi!) Swagger document generation. The code will have this already set up with a link to learn more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;So now we want a Swagger UI, there are options, we could use the battle tested Swashbuckle, the popular ReDoc, or the FOSS Scalar &lt;a href="https://github.com/scalar/scalar" rel="noopener noreferrer"&gt;https://github.com/scalar/scalar&lt;/a&gt; however they do have a cloud offering. If you pay you get the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write your API documentation and publish your API references (free)&lt;/li&gt;
&lt;li&gt;Get SSL and a super cool *.apidocumentation.com subdomain (free)&lt;/li&gt;
&lt;li&gt;Write free text documentation (paid)&lt;/li&gt;
&lt;li&gt;Collaborate with your whole team (paid)&lt;/li&gt;
&lt;li&gt;Use any domain (paid)&lt;/li&gt;
&lt;li&gt;SDK generation (paid)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To set Scalar up, do the following in your Aspire AppHost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add Scalar API Reference&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scalar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddScalarApiReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ScalarTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Laserwave&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Register services with the API Reference&lt;/span&gt;
&lt;span class="n"&gt;scalar&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithApiReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One really cool thing to note, under the hood this uses their proxy which gets around CORS issues!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scalar for Aspire includes a built-in proxy that is enabled by default to provide seamless integration with your services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Proxy Deep Dive
&lt;/h3&gt;

&lt;p&gt;Aspire code is here &lt;a href="https://github.com/scalar/scalar/tree/main/integrations/aspire" rel="noopener noreferrer"&gt;https://github.com/scalar/scalar/tree/main/integrations/aspire&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They have a custom endpoint they register at &lt;code&gt;/scalar-proxy&lt;/code&gt; which uses YARP &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-9.0" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-9.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They also have a hosted service at: &lt;a href="https://proxy.scalar.com/" rel="noopener noreferrer"&gt;https://proxy.scalar.com/&lt;/a&gt;. Unrelated but other services they seem to have for development: &lt;a href="https://void.scalar.com/" rel="noopener noreferrer"&gt;https://void.scalar.com/&lt;/a&gt;, &lt;a href="https://galaxy.scalar.com/" rel="noopener noreferrer"&gt;https://galaxy.scalar.com/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Back at The Ranch
&lt;/h3&gt;

&lt;p&gt;There are a variety of other Aspire and configuration options you can check out: &lt;a href="https://guides.scalar.com/scalar/scalar-api-references/integrations/net-aspire" rel="noopener noreferrer"&gt;https://guides.scalar.com/scalar/scalar-api-references/integrations/net-aspire&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, you should be able to run your app and see that the Scalar service is up and running:&lt;/p&gt;

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

&lt;p&gt;Clicking the link gets you to the docs:&lt;/p&gt;

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

&lt;p&gt;I am not the worlds biggest fan of the theme but I set &lt;code&gt;WithTheme&lt;/code&gt; to show off the capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI
&lt;/h2&gt;

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

&lt;p&gt;This tutorial uses OpenRouter. If you are not familiar with it, its essentially a gateway to thousands of other models through a single interface. You load your account with credits and they bill you depending on the provider you want. For example throw $10 in your account and you can call Grok, Gemini, Claude, and you don't need accounts with them or to manage multiple keys. &lt;a href="https://www.linqpad.net/" rel="noopener noreferrer"&gt;LinqPad&lt;/a&gt; recently added built-in support for OpenRouter as well.&lt;/p&gt;

&lt;p&gt;Head over to OpenRouter and generate a key &lt;a href="https://openrouter.ai/settings/keys" rel="noopener noreferrer"&gt;https://openrouter.ai/settings/keys&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have that, right click your ApiService, click "User Secrets" and paste this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"OpenAIKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-or-v1-supa-secret"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the OpenAI IChatClient. Also, for an overview of the current Microsoft .NET AI landscape this blog is awesome &lt;a href="https://roxeem.com/2025/09/08/how-to-correctly-build-ai-features-in-dotnet/" rel="noopener noreferrer"&gt;https://roxeem.com/2025/09/08/how-to-correctly-build-ai-features-in-dotnet/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is awesome too: &lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/build-chat-app?pivots=openai" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/build-chat-app?pivots=openai&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I highlighted some of the more interesting areas of the docs. If you spent a few hours reading all of that you would be pretty ramped up on AI concepts across the board. Inch-deep, mile-wide as they say. Knowing just enough to be dangerous.&lt;/p&gt;

&lt;p&gt;Add the chat service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;AddUserSecrets&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OpenAIKey"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomHeadersHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;//this is a standard 'DelegatingHandler' implementation that adds HTTP headers&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OpenAiHttpClient"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpMessageHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomHeadersHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OpenAiHttpClient"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create the IChatClient&lt;/span&gt;
    &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApiKeyCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OpenAIClientOptions&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://openrouter.ai/api/v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;Transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientPipelineTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"openrouter/auto"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CustomHeadersHandler&lt;/code&gt; is entirely optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Thanks https://stackoverflow.com/questions/79645038/define-custom-headers-using-net-6-openai-sdk&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomHeadersHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelegatingHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Optional. Site URL for rankings on openrouter.ai&lt;/span&gt;
        &lt;span class="c1"&gt;//request.Headers.Add("HTTP-Referer", "MyCustomHeaderValue");&lt;/span&gt;

        &lt;span class="c1"&gt;// Optional. Site Title for rankings on openrouter.ai&lt;/span&gt;
        &lt;span class="c1"&gt;//request.Headers.Add("X-Title", "AnotherHeaderValue");&lt;/span&gt;

        &lt;span class="c1"&gt;// Call the inner handler&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted to show how we could do what is easily done in NodeJS/TS&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://openrouter.ai/api/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;OPENROUTER_API_KEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaultHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP-Referer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;YOUR_SITE_URL&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Optional. Site URL for rankings on openrouter.ai.&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;YOUR_SITE_NAME&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Optional. Site title for rankings on openrouter.ai.&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost to the payoff! Lets use our new Chat Client!&lt;/p&gt;

&lt;p&gt;Replace the current WeatherForecast endpoint with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/weatherforecast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherForecast&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;targetDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Generate complete weather data using AI with structured output&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"""
&lt;/span&gt;            &lt;span class="n"&gt;Generate&lt;/span&gt; &lt;span class="n"&gt;realistic&lt;/span&gt; &lt;span class="n"&gt;weather&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;yyyy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MM&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;
            &lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;Celsius&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;between&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;weather&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;humor&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;absurd&lt;/span&gt; &lt;span class="n"&gt;weather&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;exist&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="n"&gt;planets&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;universes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;Be&lt;/span&gt; &lt;span class="n"&gt;creative&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;think&lt;/span&gt; &lt;span class="n"&gt;interdimensional&lt;/span&gt; &lt;span class="n"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alien&lt;/span&gt; &lt;span class="n"&gt;phenomena&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;impossible&lt;/span&gt; &lt;span class="n"&gt;physics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="s"&gt;""";
&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetResponseAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;weatherData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WeatherForecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;weatherData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemperatureC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;weatherData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GetWeatherForecast"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some cool tidbits about the above code. &lt;code&gt;GetResponseAsync&amp;lt;WeatherData&amp;gt;&lt;/code&gt; is special as the response is a C# object! This is thanks to the rather transparent handling of structured output from the LLM.&lt;/p&gt;

&lt;p&gt;Finally, open your Scalar UI, scroll down to the &lt;code&gt;/weatherforecast&lt;/code&gt; endpoint, click "Test Request", click "Send".&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>api</category>
      <category>dotnet</category>
      <category>ai</category>
    </item>
    <item>
      <title>Pocketbase API Rules</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Mon, 21 Jul 2025 13:46:46 +0000</pubDate>
      <link>https://forem.com/victorioberra/pocketbase-api-rules-1mj</link>
      <guid>https://forem.com/victorioberra/pocketbase-api-rules-1mj</guid>
      <description>&lt;p&gt;While learning &lt;a href="https://pocketbase.io/" rel="noopener noreferrer"&gt;PocketBase&lt;/a&gt;, many times there are patterns and helpful things to know that are either not in the docs, are not obvious.&lt;/p&gt;

&lt;p&gt;Namely, API rules are hard. People have &lt;a href="https://github.com/pocketbase/pocketbase/discussions/6974" rel="noopener noreferrer"&gt;built websites to help you build API rules&lt;/a&gt; for example.&lt;/p&gt;

&lt;p&gt;One thing I recently learned, is that an API rule's are translated to SQL and can create joins in unexpected (but needed ways). For example:&lt;/p&gt;

&lt;p&gt;Client call: &lt;code&gt;pb.collection('login_bonus_earned').create({ user: 'abc124', loginBonus: 'hij876' });&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;API rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginBonus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;todayStart&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginBonus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;todayEnd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@request.body.loginBonus.day&lt;/code&gt; at first glance, &lt;code&gt;day&lt;/code&gt; is NOT in the body of the request. It automatically triggers a join due to &lt;code&gt;loginBonus&lt;/code&gt; being a relation, which matches the foreign key provided by the client. Resulting in this join:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;login_bonus_earned__pb_create__OKR3fS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'__temp_id____pb_create__OKR3fS'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'qa5d6btux2iqwwb'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'38ivyu8qk4tenot'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;loginBonus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;login_bonus_earned__pb_create__OKR3fS&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;login_bonus&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;__data_login_bonus_loginBonus&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;__data_login_bonus_loginBonus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'38ivyu8qk4tenot'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'qa5d6btux2iqwwb'&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="s1"&gt;'qa5d6btux2iqwwb'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'qa5d6btux2iqwwb'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;__data_login_bonus_loginBonus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-07-20 00:00:00.000Z'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt;
  &lt;span class="n"&gt;__data_login_bonus_loginBonus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-07-20 23:59:59.999Z'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>pocketbase</category>
    </item>
    <item>
      <title>Smart Home MCP Server with VSCode Copilot</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Wed, 16 Apr 2025 18:19:09 +0000</pubDate>
      <link>https://forem.com/victorioberra/smart-home-mcp-server-with-vscode-copilot-3ckj</link>
      <guid>https://forem.com/victorioberra/smart-home-mcp-server-with-vscode-copilot-3ckj</guid>
      <description>&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;My short definition is: A protocol that lets LLMs talk to your code.&lt;/p&gt;

&lt;p&gt;For example, if you have an API that returns a list of warehouses, how could you get Copilot to call your API and return a list of those warehouses? Or use the list internally to answer your questions about your warehouses?&lt;/p&gt;

&lt;p&gt;The answer is MCP. These clients can use it &lt;a href="https://modelcontextprotocol.io/clients" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/clients&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VSCode is in that list. They have added support for MCP &lt;a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/copilot/chat/mcp-servers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What can I write my MCP server in?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python SDK&lt;/li&gt;
&lt;li&gt;TypeScript SDK&lt;/li&gt;
&lt;li&gt;Java SDK&lt;/li&gt;
&lt;li&gt;Kotlin SDK&lt;/li&gt;
&lt;li&gt;C# SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using CSharp
&lt;/h2&gt;

&lt;p&gt;I followed this tutorial, I will simplify here and adapt for my Smart Home&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a .NET app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dotnet new console -n SmartHomeMCPServer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dotnet add package ModelContextProtocol&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dotnet add package Microsoft.Extensions.Logging.Console&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dotnet add package Microsoft.Extensions.Hosting&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotnet add package RestEase&lt;/code&gt; (http client helper &lt;a href="https://github.com/canton7/RestEase" rel="noopener noreferrer"&gt;https://github.com/canton7/RestEase&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am using a Hubitat with an API plugin &lt;a href="https://hubitat.com/" rel="noopener noreferrer"&gt;https://hubitat.com/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is what the API plugin shows me:&lt;/p&gt;

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

&lt;p&gt;I used Copilot edit mode to generate my app after feeding in the API spec above. I also fed it the RestEase docs which is just the entire README.md. This is the HTTP client it generated with RestEase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IHubitatApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apps/api/60/devices/all"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllDevicesAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apps/api/60/devices/{deviceId}/{command}/{secondaryValue}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendDeviceCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"deviceId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secondaryValue"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secondaryValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apps/api/60/devices/{deviceId}/{command}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendDeviceCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"deviceId"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Device&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Room&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Manufacturer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Capabilities&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Attributes&lt;/span&gt; &lt;span class="n"&gt;Attributes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Commands&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, I wrapped the client around an MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HubitatTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fetches all devices from the Hubitat API."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllDevices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://192.168.1.120"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-TOKEN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RestClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHubitatApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllDevicesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Send a command to a specific device."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendDeviceCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;secondaryValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://192.168.1.120"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR-TOKEN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RestClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHubitatApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Ensure secondaryValue is handled properly&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secondaryValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendDeviceCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendDeviceCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secondaryValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I "vibe coded" the above, in a real world scenario id have a .env file for tokens and URLs.&lt;/p&gt;

&lt;p&gt;Finally, you can ask the LLM to list your devices!&lt;/p&gt;

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

</description>
    </item>
    <item>
      <title>Easy Car Wash Tips</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Fri, 14 Mar 2025 18:11:29 +0000</pubDate>
      <link>https://forem.com/victorioberra/easy-car-wash-tips-499g</link>
      <guid>https://forem.com/victorioberra/easy-car-wash-tips-499g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1aznewyau82nw8mdhf42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1aznewyau82nw8mdhf42.png" alt="Ford Explorer ST" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools of the trade:
&lt;/h2&gt;

&lt;p&gt;None of the below are affiliate links! And any of these can probably be purchased cheaper at your local hardware store. Amazon links just for reference and images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Washing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Microfiber Mitts&lt;/li&gt;
&lt;li&gt;Pressure washer: &lt;a href="https://www.amazon.com/RYOBI-Electric-Pressure-Washer-RY141820VNM" rel="noopener noreferrer"&gt;https://www.amazon.com/RYOBI-Electric-Pressure-Washer-RY141820VNM&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;40 Degree Nozzle: &lt;a href="https://www.amazon.com/McKillans-Stainless-Rinsing-Rubberized-Protecting/dp/B0BQZ2DGPP" rel="noopener noreferrer"&gt;https://www.amazon.com/McKillans-Stainless-Rinsing-Rubberized-Protecting/dp/B0BQZ2DGPP&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Foam Cannon: &lt;a href="https://www.amazon.com/Ryobi-RY31F04-Pressure-Washer-Blaster/dp/B088WD7NRZ" rel="noopener noreferrer"&gt;https://www.amazon.com/Ryobi-RY31F04-Pressure-Washer-Blaster/dp/B088WD7NRZ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Soap (Mr Pink): &lt;a href="https://www.amazon.com/Chemical-Guys-Mr-Pink-Car-Wash-Soap/dp/B071RKWXKV" rel="noopener noreferrer"&gt;https://www.amazon.com/Chemical-Guys-Mr-Pink-Car-Wash-Soap/dp/B071RKWXKV&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Waxing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tons of microfiber cloths, I go through several during a waxing: &lt;a href="https://www.amazon.com/50-Pack-SimpleHouseware-Microfiber-Cleaning/dp/B01NAAJLVG" rel="noopener noreferrer"&gt;https://www.amazon.com/50-Pack-SimpleHouseware-Microfiber-Cleaning/dp/B01NAAJLVG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chemical-Guys Wax: &lt;a href="https://www.amazon.com/Chemical-Guys-Butter-Wet-Wax-Paste-Cream/dp/B00FALVU8A" rel="noopener noreferrer"&gt;https://www.amazon.com/Chemical-Guys-Butter-Wet-Wax-Paste-Cream/dp/B00FALVU8A&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CAR GUYS Plastic Restorer: &lt;a href="https://www.amazon.com/dp/B071FRWWRF" rel="noopener noreferrer"&gt;https://www.amazon.com/dp/B071FRWWRF&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Nice to haves
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Under car pressure washer: &lt;a href="https://www.amazon.com/Pressure-Washer-Undercarriage-Professional-Grade-Attachment/dp/B0CL9Y5KZ3" rel="noopener noreferrer"&gt;https://www.amazon.com/Pressure-Washer-Undercarriage-Professional-Grade-Attachment/dp/B0CL9Y5KZ3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Silicone Grease (for wiper blades): &lt;a href="https://www.amazon.com/Super-Lube-92003-Lubricating-Translucent/dp/B0081JE0OO" rel="noopener noreferrer"&gt;https://www.amazon.com/Super-Lube-92003-Lubricating-Translucent/dp/B0081JE0OO&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rubber Seal Protectant (plasticizer): &lt;a href="https://www.amazon.com/dp/B00T44D1R2" rel="noopener noreferrer"&gt;https://www.amazon.com/dp/B00T44D1R2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Initial rinse
&lt;/h2&gt;

&lt;p&gt;Use the 40 degree nozzle about 2 feet away from the paint. &lt;strong&gt;Important, if it hurts your hand, it hurts the paint!&lt;/strong&gt; Do not pressure wash damaged areas of a car, and aim to avoid seals and gaps where pressurized water would enter in places rain normally would not. Also I would not use anything less than 40 degrees as anything tighter could damage the paint or increase damages in problem areas (rock chips, scratches, rust).&lt;/p&gt;

&lt;p&gt;Wash top-down. I usually start at the windows and work my way down. A lot of times for bird-poop and other stuck on times an initial once-over softens it, and then come back a after rinsing other parts and it blasts right off. Don't try to laser dry/hardened right off the bat.&lt;/p&gt;

&lt;p&gt;Its safe to hit your rims, and carefully wash out your wheel wells as well. Knocks down a lot of salt and dirt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soap
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make sure you test the foam cannon on the ground first.&lt;/strong&gt; I noticed unless you adjust the cannon I linked, it will be in laser mode that can blast a line into concrete.&lt;/p&gt;

&lt;p&gt;Some cannons demand a specific soap and ratio. Mr Pink seems to work great in anything and you may want to play with ratios to nail down what works best.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do the cars baseboards and near the tires last! Use an entirely different glove to wash your tires and VS your car paint and store them separate&lt;/strong&gt;. The reason being brake dust is basically sandpaper and it will destroy your paint. Plus all the horrible road dust, dirt, gravel, asphalt. Not stuff you want to go over in a glove and then buff into your paint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wax
&lt;/h2&gt;

&lt;p&gt;Any wax is going to work well. I have used Turtle Wax and Meguiar's. I am now trying the wax I linked above by Chemical-Guys which is liquid. I think I prefer solid but it works well.&lt;/p&gt;

&lt;p&gt;You may not have a choice but its best to wax in the shade. You want the wax to find its way into all those swirls, scratches and imperfections of the paint. The sun will insta-dry it out.&lt;/p&gt;

&lt;p&gt;Common questions: yes its totally OK to wax your headlights and taillights! This will protect them against UV damage and bug splatters. Yes its totally OK to wax your rims! Obviously avoid the caliper/rotors and make sure you wash the rims WELL. As waxing on top of brake pad dust is literally sandpaper.&lt;/p&gt;

&lt;p&gt;The best part about waxing is you can do a few "maintenance washes" with just water later and knock down so much dirt and bugs that slide right off the wax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plastic Restorer
&lt;/h2&gt;

&lt;p&gt;This stuff is awesome. It can make 10 year old plastic brand new. It protects from UV damage and deepens and darkens the black on your plastic. Cant recommend enough. Check out pics on Amazon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rubber Seal Restorer
&lt;/h3&gt;

&lt;p&gt;I haven't used these too much but as your seals age and rubber ages, a plasticizer is a helpful treatment to restore  your cars seals. Door seals, window seals, trunk seals, etc. Personally id only use this for older seals until I have seen data that products like these do not actually increase the rate at which seals dry out without frequent re-applications of products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiper Blade Tips
&lt;/h2&gt;

&lt;p&gt;Buy some silicone grease, I like Super Lube, and wipe down your wiper blades with it. RainX sells blades they call Silicone AdvantEdge™️ this is literally just silicone grease on the blade. And they charge a LOT more for these blades. &lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Make sure you add pump protector before you store your pressure washer for the season. If you have a garage, I have seen dudes mount theirs to the wall and have a little shelf for different nozzles and their soap blaster and stuff.&lt;/p&gt;

&lt;p&gt;Wash out your cannons with fresh water. Soap can dry and harden and ruin nozzles and cannon siphons and filters.&lt;/p&gt;

&lt;p&gt;Send all your mitts through the washing machine. Hand-wash wax soaked rags in warm soapy water. You do not want your washing machine pump soaked in wax water.&lt;/p&gt;

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

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Dapper mappings, which is best?</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Wed, 15 May 2024 16:03:50 +0000</pubDate>
      <link>https://forem.com/victorioberra/dapper-mappings-which-is-best-10le</link>
      <guid>https://forem.com/victorioberra/dapper-mappings-which-is-best-10le</guid>
      <description>&lt;p&gt;Like many, our Database columns look like this &lt;code&gt;customer_id&lt;/code&gt;. In CSharp, guidelines suggest using &lt;code&gt;CustomerId&lt;/code&gt; for properties.&lt;/p&gt;

&lt;p&gt;The first thing people will tell you is to just alias the column in SQL. IE: &lt;code&gt;SELECT customer_id AS CustomerId ...&lt;/code&gt;. This works great... unless you are calling a SPROC. In many environments, making changes to SPROCS that could be called by other SPROCS or countless downstream apps is completely out of the question.&lt;/p&gt;

&lt;p&gt;There are 3 ways I have learned to map.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use dynamic.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://github.com/henkmollema/Dapper-FluentMap"&gt;Dapper.FluentMap&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Use custom &lt;code&gt;[Column]&lt;/code&gt; attribute &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Use dynamic
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CommandDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;commandText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;commandType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoredProcedure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;queryResult&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SponsorGuestRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above, &lt;code&gt;x&lt;/code&gt; is dynamic, which is what &lt;code&gt;.QueryAsync()&lt;/code&gt; returns if you do not give it a type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros:
&lt;/h3&gt;

&lt;p&gt;No external libs, no additional configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons:
&lt;/h3&gt;

&lt;p&gt;Not using the Generic &lt;code&gt;QueryAsync&amp;lt;&amp;gt;&lt;/code&gt; means you're manually doing all the mapping. You could forget to map a column. This might as well be a step above ADO.NET if you aren't using Dapper to turn SQL into POCOs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use Dapper.FluentMap
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerMap&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EntityMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CustomerMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;FluentMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CustomerMap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pros:
&lt;/h3&gt;

&lt;p&gt;Very familiar syntax to anyone who has written mappers, or used FluentValidation, or used Entity Framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons:
&lt;/h3&gt;

&lt;p&gt;You have to remember to call &lt;code&gt;.Initialize()&lt;/code&gt; and provide all entity maps, you could maybe use Scrutor to scan and auto-register them? Mapping logic is now less discoverable and spread out from the classes themselves. Registration is static and global, not sure how to explain it but AutoMapper has me inject &lt;code&gt;IMapper&lt;/code&gt;. Doing any sort of global static setup of dependencies feels dirty in 2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use custom &lt;code&gt;[Column]&lt;/code&gt; attribute
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via https://stackoverflow.com/questions/8902674/manually-map-column-names-with-class-properties&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ColumnAttributeTypeMapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FallbackTypeMapper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ColumnAttributeTypeMapper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SqlMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ITypeMap&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CustomPropertyTypeMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                        &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                            &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCustomAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ColumnAttribute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;columnName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultTypeMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pros:
&lt;/h3&gt;

&lt;p&gt;Highly familiar &lt;code&gt;[Column()]&lt;/code&gt; syntax. Highly discoverable, mapping is right on the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cons:
&lt;/h3&gt;

&lt;p&gt;Custom code must now be maintained, and probably tested. Why is this not just in Dapper?&lt;/p&gt;

&lt;p&gt;Personally, I like the &lt;code&gt;[Column()]&lt;/code&gt; feature. What are you using?&lt;/p&gt;

</description>
      <category>dapper</category>
      <category>csharp</category>
      <category>net</category>
      <category>sql</category>
    </item>
    <item>
      <title>AWS Lightsail Container Services with Reverse Proxy</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Sun, 01 Jan 2023 22:17:00 +0000</pubDate>
      <link>https://forem.com/victorioberra/aws-lightsail-container-services-with-reverse-proxy-4bk</link>
      <guid>https://forem.com/victorioberra/aws-lightsail-container-services-with-reverse-proxy-4bk</guid>
      <description>&lt;h2&gt;
  
  
  Including: SSL, custom domains and Route 53
&lt;/h2&gt;

&lt;p&gt;Today I will show you how to leverage AWS Lightsail Container Services to create an environment where you have 2 public services behind a reverse proxy, and a database.&lt;/p&gt;

&lt;p&gt;I host my domain in AWS Route 53. I use Lightsail Custom Domains to both assign custom domains to my container service, and to create a certificate.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Caddy? Logto?
&lt;/h2&gt;

&lt;p&gt;The reverse proxy is &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Caddy will proxy 2 apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An app called &lt;a href="https://github.com/logto-io/logto" rel="noopener noreferrer"&gt;LogTo&lt;/a&gt; think self-hosted Auth0.&lt;/li&gt;
&lt;li&gt;An app called &lt;a href="https://github.com/traefik/whoami" rel="noopener noreferrer"&gt;whoami&lt;/a&gt;. This is a dead simple go app that spits back header and IP information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the real world, you would host an app instead of whoami that is protected with logto.&lt;/p&gt;

&lt;p&gt;Lastly a Postgres container for this example. You should probably use the Lightsail Postgres database offering, at time of writing it is $10/mo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Grab the AWS CLI, configure it, and the &lt;a href="https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-install-software" rel="noopener noreferrer"&gt;lightsail cli plugin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install docker, I did this all on Windows 10 with WSL2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use a mix of the GUI and CLI. You could do it all in the GUI, or CLI.&lt;/p&gt;

&lt;p&gt;I specify my AWS region as Ohio, because when I created my container service, it created it in Ohio (us-east-2). You might have to change your commands to reflect your region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buy a domain on Route53
&lt;/h2&gt;

&lt;p&gt;Do not delete NS, or SOA records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a container service
&lt;/h2&gt;

&lt;p&gt;Head over to lightsail and create a container service, or use the CLI. Beware windows users, for the CLI I had to follow a guide &lt;a href="https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-install-software" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and the CLI still wouldn't work for me. Ended up moving the lightsail exe to another bin folder for something like git to get it to work. I might have clobbered my environment vars with their &lt;code&gt;setx&lt;/code&gt; command recommendation too...&lt;/p&gt;

&lt;p&gt;A micro container service worked just fine for me. $10/mo at time of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a cert, and some custom domains
&lt;/h2&gt;

&lt;p&gt;For the cert and custom domains: Create example.com, &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;, logto.example.com and reserved.example.com. I created reserved in case I needed a new domain.&lt;/p&gt;

&lt;p&gt;Create the DNS records, go to your lightsail instance and grab the Public domain (&lt;code&gt;&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com&lt;/code&gt;) you will need the lightsail hosted zone id, list here: &lt;a href="https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-route-53-alias-record-for-container-service" rel="noopener noreferrer"&gt;https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-route-53-alias-record-for-container-service&lt;/a&gt; for example, for &lt;code&gt;US East (Ohio) (us-east-2)&lt;/code&gt; it is &lt;code&gt;Z10362273VJ548563IY84&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a file &lt;code&gt;change-resource-record-sets.json&lt;/code&gt; this will be used by the AWS CLI to create a bunch of DNS records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceRecordSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AliasTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"HostedZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MYHOSTEDZONEID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DNSName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"EvaluateTargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceRecordSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"www.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AliasTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"HostedZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MYHOSTEDZONEID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DNSName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"EvaluateTargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceRecordSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logto.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AliasTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"HostedZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MYHOSTEDZONEID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DNSName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"EvaluateTargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceRecordSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reserved.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AliasTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"HostedZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MYHOSTEDZONEID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DNSName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"EvaluateTargetHealth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;deploy with &lt;code&gt;aws route53 change-resource-record-sets --hosted-zone-id MYHOSTEDZONEID --change-batch file://change-resource-record-sets.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Don't forget to attach the cert to your instance!&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and publish images to Lightsail Container Services Image Store
&lt;/h2&gt;

&lt;p&gt;Create a Caddy, Logto, and Postgres docker file each in their own folders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caddy
&lt;/h3&gt;

&lt;p&gt;Create a folder for /Caddy, create a file called &lt;code&gt;Caddyfile&lt;/code&gt;, put the following in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com:80 {
    reverse_proxy localhost:3001 {
        trusted_proxies private_ranges
    }
}

logto.example.com:80 {
    reverse_proxy localhost:3002 {
        trusted_proxies private_ranges
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the docker file &lt;code&gt;/Caddy/Dockerfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;FROM caddy:latest&lt;/span&gt;

&lt;span class="s"&gt;COPY Caddyfile /etc/caddy/Caddyfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Logto
&lt;/h3&gt;

&lt;p&gt;Create a docker file &lt;code&gt;/Logto/Dockerfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;FROM ghcr.io/logto-io/logto:prerelease&lt;/span&gt;

&lt;span class="s"&gt;ENTRYPOINT ["sh", "-c", "npm run cli db seed -- --swe &amp;amp;&amp;amp; npm start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: You need to call &lt;code&gt;db seed --swe&lt;/code&gt; with LogTo so it will connect to Postgres, and create/seed the database. &lt;code&gt;--swe&lt;/code&gt; means "skip when exists".&lt;/p&gt;

&lt;h3&gt;
  
  
  Postgres
&lt;/h3&gt;

&lt;p&gt;Create a docker file &lt;code&gt;/Postgres/Dockerfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;FROM postgres:14-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Yeah... I know. I had to do this. I could NOT just use &lt;code&gt;registry.hub.docker.com/postgres:14-alpine&lt;/code&gt;, I had to create and push my own containers. Otherwise, Lightsail Container Services will refuse to start, the error log looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1/Jan/2022:01:03:21] [deployment:8] Creating your deployment
[1/Jan/2022:01:04:31] [deployment:8] Started 1 new node
[1/Jan/2022:01:05:25] [deployment:8] Started 1 new node
[1/Jan/2022:01:06:34] [deployment:8] Started 1 new node
[1/Jan/2022:01:07:12] [deployment:8] Canceled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the googling I did was unhelpful. I saw someone mention I should publish container images to my Lightsail container services image repo... and that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cd Caddy; docker build -t caddy-container .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd Postgres; docker build -t postgres-container .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd Logto; docker build -t logto-container .&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Publish the images to Lightsail Container Registry
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aws lightsail push-container-image --region us-east-2 --service-name myservice --label caddy-container --image caddy-container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws lightsail push-container-image --region us-east-2 --service-name myservice --label postgres-container --image postgres-container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aws lightsail push-container-image --region us-east-2 --service-name myservice --label logto-container --image logto-container&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finally, create the container service
&lt;/h2&gt;

&lt;p&gt;Create 2 JSON files. &lt;code&gt;containers.json&lt;/code&gt; and &lt;code&gt;public-endpoint.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;containers.json&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Environment variables are super important! Change them to whatever you need. Also image names!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"whoami-3001"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registry.hub.docker.com/containous/whoami"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--port"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3001"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;":myservice.postgres-container.NUMBERHERE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"POSTGRES_USER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresuser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"POSTGRES_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yoursecretpassword1234"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;":myservice.logto-container.NUMBERHERE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TRUST_PROXY_HEADER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DB_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres://postgresuser:yoursecretpassword1234@localhost:5432/logto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ENDPOINT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://logto.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"NODE_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"caddy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;":myservice.caddy-container.NUMBERHERE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"80"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for &lt;code&gt;public-endpoint.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"containerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"caddy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"containerPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy!
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws lightsail create-container-service-deployment --region us-east-2 --service-name myservice --containers file://containers.json --public-endpoint file://public-endpoint.json&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions/Pain-points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No way in UI to delete a certificate? What if I need to add a new subdomain to my cert? I saw the CLI has delete-certificate, I tried and got &lt;code&gt;Distribution-related APIs are only available in the us-east-1 Region. Please set your Region configuration to us-east-1 to create, view, or edit distribution resources.&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;Installing the Amazon Lightsail container services plugin for Windows is difficult. Compare this to installing an Azure CLI extension. &lt;code&gt;az extension add --name &amp;lt;extension-name&amp;gt;&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;Postgres would not launch inside Lightsail when using public image (&lt;code&gt;registry.hub.docker.com/postgres:14-alpine&lt;/code&gt;). I had to create my own blank docker file as shown above, and deploy it to the Lightsail container registry and it worked perfect. Still super confused by this one...&lt;/li&gt;
&lt;li&gt;The public endpoint is doing SSL termination, and setting &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers in addition to allowing traffic from my custom domain. This means there is effectively a load balancer in front of my containers. But there does not seem to be docs explaining this. For example, Caddy is listening on port 80, and I expose HTTP port 80 as my public pot, but ALL traffic to my app comes in through SSL 443, and &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; is even &lt;code&gt;https&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can only have 4 custom domains pointed to your container service... if I hosted two more public apps in my container service, id be SOL.&lt;/li&gt;
&lt;li&gt;What is a public port on a container exactly? My containers seem to be able to talk without me needing to use public ports. For example, logto can talk to Postgres without issue. Same with the "whoami" container. If I can set multiple public ports, then why can I only set a single public endpoint to one of these ports? What does public ports even do?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DNS notes
&lt;/h2&gt;

&lt;p&gt;Route 53 is an easy powerful way to maintain your domain. Docs will advise you use Lightsail Container Services to host the domain, but unless you register your domain using Lightsail in the first place, you will need to wait 48 hours for the nameservers to update for Lightsail to have control.&lt;/p&gt;

&lt;p&gt;I chose to keep my DNS on Route 53. But I did have issues with my Apex domain. I tried following the docs here &lt;a href="https://Lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-Lightsail-route-53-alias-record-for-container-service" rel="noopener noreferrer"&gt;https://Lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-Lightsail-route-53-alias-record-for-container-service&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Somewhat odd they say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You can route traffic for a registered domain, such as example.com, to the applications running on a Lightsail container service. You do this by adding an alias record to the hosted zone of your domain that points to the default domain of your Lightsail container service. &lt;strong&gt;You can do this only by using the AWS Command Line Interface (AWS CLI). It cannot be done using the Route 53 console.&lt;/strong&gt;".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They also say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"To use the root of your domain with your Lightsail container service, you must specify an @ symbol in the subdomain space of your domain (for example, @.example.com)."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried this, and it did not work for me at all. In fact, simply creating an A record pointing to my Lightsail alias &lt;code&gt;&amp;lt;service-name&amp;gt;.&amp;lt;random-guid&amp;gt;.&amp;lt;aws-region-name&amp;gt;.cs.amazonLightsail.com&lt;/code&gt; worked for me.&lt;/p&gt;

&lt;p&gt;I created an issue on the AWS form (re:Post) and have not yet received a reply. &lt;a href="https://repost.aws/questions/QU-y7P6lmzQZKj-QasRd8MyA/lightsail-container-services-with-an-apex-domain-in-route-53" rel="noopener noreferrer"&gt;https://repost.aws/questions/QU-y7P6lmzQZKj-QasRd8MyA/lightsail-container-services-with-an-apex-domain-in-route-53&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>lightsail</category>
      <category>containers</category>
      <category>caddy</category>
    </item>
    <item>
      <title>Auth0 Custom Google Social Login Connection</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Wed, 16 Feb 2022 02:13:57 +0000</pubDate>
      <link>https://forem.com/victorioberra/auth0-custom-google-social-login-connection-1egg</link>
      <guid>https://forem.com/victorioberra/auth0-custom-google-social-login-connection-1egg</guid>
      <description>&lt;p&gt;The Auth0 free tier only lets you have a couple social connections. If you use their Google one, then you have used up one of your connections.&lt;/p&gt;

&lt;p&gt;We can create a custom social connection, and connect it to Google.&lt;/p&gt;

&lt;p&gt;Head to GCP&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click API &amp;amp; Services&lt;/li&gt;
&lt;li&gt;Click OAuth Consent Screen&lt;/li&gt;
&lt;li&gt;Fill out the requested info, put whatever you want for the URLs, just make sure you add the domain auth0.com&lt;/li&gt;
&lt;li&gt;Add the top 3 basic scopes: profile, email, openid&lt;/li&gt;
&lt;li&gt;Add test users&lt;/li&gt;
&lt;li&gt;save it all, and click "Credentials" on the left navigation menu&lt;/li&gt;
&lt;li&gt;Create an OAuth Client Id, for redirect URI use: &lt;a href="https://YOUR_TENANT.us.auth0.com/login/callback"&gt;https://YOUR_TENANT.us.auth0.com/login/callback&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now head over to Auth0, click "Authentication" -&amp;gt; "Social" -&amp;gt; Create&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scroll to bottom of all the options, click "Create Custom"&lt;/li&gt;
&lt;li&gt;name it "GCP-whatever-you-want", might be a good idea to put your GCP project name or something&lt;/li&gt;
&lt;li&gt;Use the URLs they gave you on the Credentials screen in GCP&lt;/li&gt;
&lt;li&gt;Authorization URL: &lt;a href="https://accounts.google.com/o/oauth2/auth"&gt;https://accounts.google.com/o/oauth2/auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Token: &lt;a href="https://oauth2.googleapis.com/token"&gt;https://oauth2.googleapis.com/token&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, use this user info script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id_token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID_Token from Google has the two most important things we need: sub, and email.&lt;/p&gt;

&lt;p&gt;the access_token wont however.&lt;/p&gt;

&lt;p&gt;Happy developing! &lt;/p&gt;

</description>
      <category>google</category>
      <category>auth0</category>
      <category>oidc</category>
      <category>gcp</category>
    </item>
    <item>
      <title>Use the .NET Core AWS SDK on Blazor WASM</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Mon, 25 Jan 2021 15:38:41 +0000</pubDate>
      <link>https://forem.com/victorioberra/use-the-net-core-aws-sdk-on-blazor-wasm-13hf</link>
      <guid>https://forem.com/victorioberra/use-the-net-core-aws-sdk-on-blazor-wasm-13hf</guid>
      <description>&lt;p&gt;The .NET &lt;a href="https://www.nuget.org/packages/AWSSDK.Core/" rel="noopener noreferrer"&gt;Core AWS SDK&lt;/a&gt; has released an update that allows it to run fully in the browser thanks to WASM.&lt;/p&gt;

&lt;p&gt;In this post, id like to show an example that enumerates S3 objects fully client side! You should also be able to utilize services like &lt;a href="https://aws.amazon.com/cognito/" rel="noopener noreferrer"&gt;Cognito&lt;/a&gt; and &lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FdDKV2vD.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FdDKV2vD.png" alt="Screenshot of Blazor WASM S3 Object Browser"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before we get started, id like to point out some caveats that I ran into while testing this functionality.&lt;/p&gt;

&lt;p&gt;⚠ .NET 5 not yet supported! This is because Crypto libraries no longer exist in .NET 5 WASM, so AWS can't sign requests. &lt;a href="https://github.com/dotnet/runtime/issues/40074" rel="noopener noreferrer"&gt;More info here.&lt;/a&gt;&lt;br&gt;
⚠ Today, you &lt;strong&gt;must&lt;/strong&gt; use the &lt;code&gt;UseAlternateUserAgentHeader&lt;/code&gt; wherever possible. More on this below.&lt;/p&gt;

&lt;p&gt;Lets get started!&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Grab the latest Core 3.1 SDK: &lt;a href="https://dotnet.microsoft.com/download/dotnet-core/3.1" rel="noopener noreferrer"&gt;https://dotnet.microsoft.com/download/dotnet-core/3.1&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  AWS (bucket &amp;amp; user)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an S3 bucket (or use an existing)&lt;/li&gt;
&lt;li&gt;Add a few objects&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;❗ Add a CORS policy. Remember, you're in the browser now! The AWS SDK requests to the AWS API will show up right in the networking tab of your browser's dev tools. Very cool!&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedMethods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedOrigins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ExposeHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxAgeSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Create an IAM user and select "Programmatic access", apply an existing policy called &lt;code&gt;AmazonS3ReadOnlyAccess&lt;/code&gt; to the user, download the resulting credentials csv or copy the access key ID and secret and store somewhere.&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;🎉 &lt;strong&gt;You are ready to code!&lt;/strong&gt; 🎊&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Create a folder, and inside place a &lt;code&gt;global.json&lt;/code&gt; with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sdk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.1.403"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the dotnet CLI that any dotnet commands you run inside that folder should target 3.1.&lt;/p&gt;

&lt;p&gt;Next, scaffold out a new blazor project by running the following commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dotnet new blazorwasm --name s3blazorwasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd s3blazorwasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dotnet add package AWSSDK.S3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWSSDK.S3 has a dependency on AWSSDK.Core.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;_imports.razor&lt;/code&gt; import the AWS stuff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@using Amazon
@using Amazon.S3
@using Amazon.S3.Model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For speed and brevity, open up &lt;code&gt;Counter.razor&lt;/code&gt; clear out everything and place the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@page&lt;/span&gt; &lt;span class="s"&gt;"/counter"&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;S3&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="n"&gt;Objects&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nf"&gt;@if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucketObjectNames&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;...&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;@foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bucketObjectName&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bucketObjectNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;@bucketObjectName&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;@code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bucketObjectNames&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnInitializedAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AmazonS3Config&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;RegionEndpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RegionEndpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USEast1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UseAlternateUserAgentHeader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;s3Client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AmazonS3Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"YOURKEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"YOURSECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bucketObjectResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;s3Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListObjectsV2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ListObjectsV2Request&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;BucketName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOURBUCKET"&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;bucketObjectNames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucketObjectResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S3Objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ℹ Special note on &lt;code&gt;UseAlternateUserAgentHeader&lt;/code&gt;. Internally, the AWS SDK generates a user agent on its own and includes it as part of the signature. When your browser makes a request it ends up using the User-Agent that Chrome generates. This fails the signature check. You need &lt;code&gt;UseAlternateUserAgentHeader&lt;/code&gt; to tell AWS to use the generated header (looks like: &lt;code&gt;x-amz-user-agent: aws-sdk-dotnet-netstandard/3.5.7.8 aws-sdk-dotnet-core/3.5.2.0 .NET_Core/(3.2-wasm/1a6e64a9381) OS/web ClientAsync&lt;/code&gt;) which gets signed, and sent in the request and validated by AWS properly.&lt;/p&gt;

&lt;p&gt;Fire up your app, you should see a list of your objects when you navigate to /counter! Of course feel free to update the page name, route and navigation.&lt;/p&gt;

&lt;p&gt;Hopefully this helps you get started, I noticed as this is so new, there are not many docs or information available on any of this yet.&lt;/p&gt;

&lt;p&gt;Also, don't forget to delete the user you created and/or remove policies from it, as well as remove the CORS policy from your bucket.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aws</category>
      <category>blazor</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Open GitExtensions from Visual Studio</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Tue, 24 Mar 2020 16:13:06 +0000</pubDate>
      <link>https://forem.com/victorioberra/open-gitextensions-from-visual-studio-2e2e</link>
      <guid>https://forem.com/victorioberra/open-gitextensions-from-visual-studio-2e2e</guid>
      <description>&lt;p&gt;If you haven't used &lt;a href="http://gitextensions.github.io/"&gt;GitExtensions &lt;/a&gt; as your Git GUI, I highly recommend it. Unlike other Git GUIs, it does not attempt to hold your hand. It is feature rich, under &lt;a href="https://github.com/gitextensions/gitextensions/commits/master"&gt;&lt;strong&gt;very&lt;/strong&gt; active development&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I try and automate or script everything I can. Something I find myself doing a lot is opening a solution using a shortcut, and then having to open a CLI or explorer and having to navigate to my solution dir and open GitExtensions.&lt;/p&gt;

&lt;p&gt;I recently learned about &lt;a href="https://docs.microsoft.com/en-us/visualstudio/ide/managing-external-tools?view=vs-2019"&gt;"External Tools" in Visual Studio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is as simple as it sounds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can call external tools from inside Visual Studio by using the Tools menu. A few default tools are available from the Tools menu, and you can customize the menu by adding other executables of your own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For GitExtensions, open "Tools" -&amp;gt; "External Tools..." and then enter the following info in the dialog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aEPwEJ0e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/erhlu71rh5x2mp4uiots.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aEPwEJ0e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/erhlu71rh5x2mp4uiots.png" alt="External Tools Dialog - Visual Studio"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And when youre done, click the Tools menu, and you should see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XsUWIqs_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/v3ualqf8xosez4fzzcvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XsUWIqs_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/v3ualqf8xosez4fzzcvr.png" alt="Tools Menu - Visual Studio"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gitextensions</category>
      <category>visualstudio</category>
      <category>externaltools</category>
      <category>git</category>
    </item>
    <item>
      <title>GitHub Actions Mini Tutorial - Only Deploy Master</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Fri, 28 Feb 2020 22:14:48 +0000</pubDate>
      <link>https://forem.com/victorioberra/github-actions-mini-tutorial-only-deploy-master-19mg</link>
      <guid>https://forem.com/victorioberra/github-actions-mini-tutorial-only-deploy-master-19mg</guid>
      <description>&lt;p&gt;I thought id share a little info on how to have a conditional statement for your actions that ONLY executes if you are on master.&lt;/p&gt;

&lt;p&gt;Firstly, only run on pushes to master, and PRs to master.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Trigger the workflow on push or pull request for master only.&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then finally, in any action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# Upload to S3&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync s3&lt;/span&gt;
      &lt;span class="c1"&gt;# Do not deploy anything other than master&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/master'&lt;/span&gt;    
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jakejarvis/s3-sync-action@master&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--acl public-read --follow-symlinks --delete&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AWS_S3_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_S3_BUCKET }}&lt;/span&gt;
        &lt;span class="na"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-east-1'&lt;/span&gt;
        &lt;span class="na"&gt;SOURCE_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dist'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The key above being: &lt;code&gt;if: github.ref == 'refs/heads/master'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Personally I wish they would make the branch name an env variable to make this much cleaner but Actions are still new.&lt;/p&gt;

&lt;p&gt;Happy deploying!&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
      <category>ciyml</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Gotchas when deploying a WebJob to Azure.</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Wed, 05 Feb 2020 18:41:28 +0000</pubDate>
      <link>https://forem.com/victorioberra/gotchas-when-deploying-a-webjob-to-azure-2ij0</link>
      <guid>https://forem.com/victorioberra/gotchas-when-deploying-a-webjob-to-azure-2ij0</guid>
      <description>&lt;h1&gt;
  
  
  Azure App Services - Deploying a .NET Core 3.1 worker as a schedule triggered WebJob via DevOps
&lt;/h1&gt;

&lt;p&gt;This article will cover some issues I ran into creating a new .NET Core 3.1 worker as a scheduled triggered WebJob on Azure App Services (AAS)&lt;/p&gt;

&lt;p&gt;You might need to be a little familiar with: .NET Core, Azure DevOps, AAS, Kudu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goals
&lt;/h2&gt;

&lt;p&gt;Lean into conventions as much as possible. Avoid complicated customization to the pipelines YAML, or deployment pipeline or unnecessary extra scripts and files in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create the AAS as Windows only because WebJobs are not yet available on Linux.&lt;/li&gt;
&lt;li&gt;Enable Always-On (so the scheduler works). You may see Core 3.1 Runtime is not on the Windows AAS according to the Azure Portal GUI but it actually is. &lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Creating the app from scratch is easy: &lt;code&gt;dotnet new worker --name MyFirst.Worker&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Create a very basic .NET Core azure-pipeline.yaml, all it needs to do is run the core publish task and then publish the artifact. The core template for DevOps does this out of the box.&lt;/p&gt;

&lt;p&gt;Use this guide: &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops"&gt;https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DotNetCoreCLI@2&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish&lt;/span&gt;
    &lt;span class="na"&gt;publishWebProjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--configuration&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(BuildConfiguration)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)'&lt;/span&gt;
    &lt;span class="na"&gt;zipAfterPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;

&lt;span class="c1"&gt;# this code takes all the files in $(Build.ArtifactStagingDirectory) and uploads them as an artifact of your build.&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)'&lt;/span&gt; 
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MyFirstWorker'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the build and verify your worker was published.&lt;/p&gt;

&lt;h3&gt;
  
  
  First gotcha:
&lt;/h3&gt;

&lt;p&gt;Always generate an Exe. Kudu looks for specific files to run. If you use the DevOps Ubuntu agent, you wont get an Exe. So how can we ALWAYS get one? &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need to edit the publish arguments in your pipeline YAML and add &lt;code&gt;-r win-x86 --self-contained false&lt;/code&gt;.&lt;/strong&gt; this will force Core to spit out an Exe for windows, even when publishing on Ubuntu. WITHOUT creating a self contained deployment. Although, there is nothing wrong with creating an SCD if you want. might even be a good idea, but for this tutorial I want to go from &lt;code&gt;dotnet new&lt;/code&gt; to a working deployment without lighting up every feature in Core and without adding more scripts and files to my repo.&lt;/p&gt;

&lt;p&gt;NOTE: To see how Kudu picks something to run, check the Wiki for WebJobs&lt;a href="https://github.com/projectkudu/kudu/wiki/WebJobs"&gt;Kudu wiki&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second gotcha:
&lt;/h3&gt;

&lt;p&gt;The path you deploy to in AAS for web jobs is highly specific. It MUST be in one of the following:&lt;/p&gt;

&lt;p&gt;For a triggered (or scheduled) job, the folder is &lt;code&gt;wwwroot\app_data\jobs\triggered\{job name}&lt;/code&gt;, and for a continuous job, it's &lt;code&gt;wwwroot\app_data\jobs\continuous\{job name}&lt;/code&gt;. &lt;strong&gt;This is easily achieved by setting the &lt;code&gt;--output&lt;/code&gt; flag on your &lt;code&gt;dotnet publish&lt;/code&gt; to publish into a directory like that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mark Heath has a great blog post on a very similar topic:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why the &lt;code&gt;app_data&lt;/code&gt; folder? Well that's a special ASP.NET folder that is intended for storing your application data. The web server will not serve up the contents of this folder, so everything in there is safe. It's also considered a special case for deployments - since it might contain application generated data files, its contents won't get deleted or reset when you deploy a new version of your app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Via Mark Heath - &lt;a href="https://markheath.net/post/managing-webjobs-with-kudu-api"&gt;https://markheath.net/post/managing-webjobs-with-kudu-api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To expand on Marks last line, in Azure DevOps Pipelines -&amp;gt; Deployments -&amp;gt; Deploy AAS Task there is actually a checkbox to blow away the app_data folder if you want. I thought that was interesting.&lt;/p&gt;

&lt;p&gt;The Kudu WebJob wiki also says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As an alternative, &lt;code&gt;d:\home\site\jobs\...&lt;/code&gt; can be used instead of &lt;code&gt;d:\home\site\wwwroot\app_data\jobs\...&lt;/code&gt;. This is useful when using Run-From-Zip, which makes &lt;code&gt;d:\home\site\wwwroot&lt;/code&gt; become read-only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But I haven't tested this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third gotcha:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Make sure to copy always the settings.job file. AND it uses &lt;a href="https://github.com/atifaziz/NCrontab"&gt;https://github.com/atifaziz/NCrontab&lt;/a&gt; which requires a seconds field.&lt;/strong&gt; I first tried &lt;code&gt;5 0 * * *&lt;/code&gt; and then had to go back and do &lt;code&gt;0 5 0 * * *&lt;/code&gt; this means run 5 minutes after midnight every day.&lt;/p&gt;

&lt;p&gt;This is trivial to verify, go to your AAS in the portal, and click the WebJob blade, you should see your schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full YAML
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ubuntu-latest'&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;buildConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release'&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DotNetCoreCLI@2&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish&lt;/span&gt;
    &lt;span class="na"&gt;publishWebProjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;False&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--configuration&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(BuildConfiguration)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--output&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)'&lt;/span&gt;
    &lt;span class="na"&gt;zipAfterPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)'&lt;/span&gt; 
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myWebsiteName'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things id like to know more about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to use Run-From-Zip WebJobs&lt;/li&gt;
&lt;li&gt;How to add a WebApp to my repo, and deploy that with my WebJob at the same time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SEO Tags: Azure,App,Services,Deploying,.NET,Core,3.1,worker,schedule,triggered,WebJob,DevOps&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>dotnet</category>
      <category>azure</category>
    </item>
    <item>
      <title>GUI for services like https://patchbay.pub/</title>
      <dc:creator>Victorio Berra</dc:creator>
      <pubDate>Tue, 07 Jan 2020 21:47:57 +0000</pubDate>
      <link>https://forem.com/victorioberra/gui-for-services-like-https-patchbay-pub-178p</link>
      <guid>https://forem.com/victorioberra/gui-for-services-like-https-patchbay-pub-178p</guid>
      <description>&lt;p&gt;&lt;a href="https://patchme.link" rel="noopener noreferrer"&gt;https://patchme.link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read patchbay.pub for the best example of how this works. The real benefit you get from this GUI is the ability for local notifications. That is to say, you can get a little ping on your desktop when a subscriber resolves. To see this in action, click "Add" and then execute the CURL command and accept notifications when prompted.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>automation</category>
      <category>http</category>
    </item>
  </channel>
</rss>
