<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michiel van Oudheusden</title>
    <description>The latest articles on DEV Community by Michiel van Oudheusden (@mivano).</description>
    <link>https://dev.to/mivano</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%2F1166975%2F76be3f46-a71c-4912-8907-6c8234a9aa7d.jpeg</url>
      <title>DEV Community: Michiel van Oudheusden</title>
      <link>https://dev.to/mivano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mivano"/>
    <language>en</language>
    <item>
      <title>Zendesk vs GitHub for Developer Support Teams: An Honest Comparison</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/zendesk-vs-github-for-developer-support-teams-an-honest-comparison-1a2c</link>
      <guid>https://dev.to/mivano/zendesk-vs-github-for-developer-support-teams-an-honest-comparison-1a2c</guid>
      <description>&lt;p&gt;Comparing Zendesk and GitHub-based support for developer teams. Where each approach wins, where it falls short, and how to decide.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub Issues as a Helpdesk: How It Actually Works</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Wed, 25 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/github-issues-as-a-helpdesk-how-it-actually-works-110k</link>
      <guid>https://dev.to/mivano/github-issues-as-a-helpdesk-how-it-actually-works-110k</guid>
      <description>&lt;p&gt;A practical walkthrough of using GitHub Issues as your customer support system, from email intake to AI triage to slash command replies.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Dev Teams Are Moving Customer Support Into GitHub</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/why-dev-teams-are-moving-customer-support-into-github-2f9g</link>
      <guid>https://dev.to/mivano/why-dev-teams-are-moving-customer-support-into-github-2f9g</guid>
      <description>&lt;p&gt;Developer teams are abandoning traditional helpdesks in favor of GitHub-based support. Here's why the shift is happening and what it means for your workflow.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Set Up Email-to-GitHub-Issues in Under 5 Minutes</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/how-to-set-up-email-to-github-issues-in-under-5-minutes-4ab7</link>
      <guid>https://dev.to/mivano/how-to-set-up-email-to-github-issues-in-under-5-minutes-4ab7</guid>
      <description>&lt;p&gt;A step-by-step guide to routing customer emails directly into GitHub Issues using Scitor. No code, no complex configuration, just forwarding and a YAML file.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Hidden Cost of Context Switching for Engineering Teams</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Wed, 04 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/the-hidden-cost-of-context-switching-for-engineering-teams-4bd7</link>
      <guid>https://dev.to/mivano/the-hidden-cost-of-context-switching-for-engineering-teams-4bd7</guid>
      <description>&lt;p&gt;Context switching between a helpdesk and your codebase costs more than you think. Here's what the research says and how to reduce it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Dynamic IIS Server Deployments with GitHub Actions</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Sun, 01 Jun 2025 22:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/dynamic-iis-server-deployments-with-github-actions-pm3</link>
      <guid>https://dev.to/mivano/dynamic-iis-server-deployments-with-github-actions-pm3</guid>
      <description>&lt;p&gt;When we began migrating our application infrastructure to the cloud, we relied on AWS and traditional Windows servers running IIS. Although containers and Kubernetes dominate today’s conversations, there are organizations that still depend on familiar, “old-school” architectures. In one recent project, we found ourselves needing to deploy a web application across multiple IIS servers behind an AWS load balancer. Our challenge was simple in statement but complex in execution: how do you run the same GitHub Actions deployment workflow on every server in a scalable, maintainable way?&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Our environment consisted of a load balancer distributing traffic to several Windows servers hosting IIS. These servers (EC2 instances) were spun up via CloudFormation, each automatically registering a GitHub self-hosted runner. In essence, every server became a target for GitHub Actions workflows.&lt;/p&gt;

&lt;p&gt;To deploy across the entire group of servers with the same tag (for example, all IIS-Server machines), we needed the workflow to run on each of them, not just on a single available runner. While setting &lt;code&gt;runs-on: [self-hosted, &amp;lt;label&amp;gt;]&lt;/code&gt; directs GitHub Actions to run the job on a runner matching that label, it does not instruct it to execute on all such servers simultaneously.&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" rel="noopener noreferrer"&gt;documented by GitHub&lt;/a&gt;, &lt;code&gt;runs-on&lt;/code&gt; selects only one matching runner; to execute a job on multiple machines, you need to use a matrix strategy.&lt;/p&gt;

&lt;p&gt;Hard-coding runner names would defeat our goal of dynamic scaling, since servers could be added or removed without updating the workflow.&lt;/p&gt;

&lt;p&gt;We looked for an approach that would adapt dynamically as servers joined or left our environment. Azure DevOps offered “&lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/release/deployment-groups/deploying-azure-vms-deployment-groups?view=azure-devops" rel="noopener noreferrer"&gt;deployment groups&lt;/a&gt;”, but GitHub Actions does not include a direct equivalent. Instead, we needed to leverage the GitHub API to query our organization’s runners at runtime, filter them by environment labels, and build a dynamic matrix of targets for our workflow. This blog post describes the story of how we achieved that goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gathering Runner Information
&lt;/h2&gt;

&lt;p&gt;The first obstacle we encountered was obtaining an authentication token capable of listing all self-hosted runners in our organization. The default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; provided to workflows only has repository scope, so it cannot retrieve organization-wide runner details. Our solution was to create a GitHub App with “Read access to organization self hosted runners”, then install it on our repository. By storing the App’s ID and private key as repository secrets (&lt;code&gt;RUNNER_TOKEN_APP_ID&lt;/code&gt; and &lt;code&gt;RUNNER_TOKEN_APP_PRIVATE_KEY&lt;/code&gt;), we could exchange these credentials for a short-lived token that the GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) could use to query the runners API.&lt;/p&gt;

&lt;p&gt;Below is the YAML snippet for the &lt;code&gt;determine-runners&lt;/code&gt; job. It runs on &lt;code&gt;ubuntu-latest&lt;/code&gt;, invokes a third-party action to fetch an application token, then uses &lt;code&gt;gh api&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; to filter runners with a specific environment label (e.g., &lt;code&gt;staging&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt;) and a fixed label (e.g., &lt;code&gt;IIS-Server&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs:
  determine-runners:
    name: Determine target runners
    runs-on: ubuntu-latest
    outputs:
      runners_json: $
    env:
      ORG_NAME: $
      TARGET_ENV_LABEL: $
      REQUIRED_LABEL_1: IIS-Server
    steps:
      - name: Get Organization API Token
        id: get_workflow_token
        uses: peter-murray/workflow-application-token-action@v4
        with:
          application_id: $
          application_private_key: $
          organization: $

      - name: Get matching runners (including offline)
        id: get_runners
        run: |
          # Retrieve all runners for the organization (up to 100 per page)
          runners_data=$(gh api "orgs/$/actions/runners?per_page=100" --jq '.runners[]')

          # Filter runners by both the fixed label and environment label
          matching_runners=$(echo "$runners_data" | jq -c \
            --arg label1 "$" \
            --arg label2 "$" \
            'select(.labels | map(.name) | (contains([$label1]) and contains([$label2])))'
          )

          # Build a JSON array of runner names
          runners_json=$(echo "$matching_runners" | jq -cs '[.[].name]')

          if [-z "$runners_json"] || ["$runners_json" == "[]" ]; then
            echo "::error::No runners found with labels: '$' and '$'."
            exit 1
          else
            echo "runners_json=$runners_json" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          fi
        env:
          GH_TOKEN: $

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the Deployment Matrix
&lt;/h2&gt;

&lt;p&gt;Once we had a JSON list of runner names in the output variable &lt;code&gt;runners_json&lt;/code&gt;, we could construct a dynamic matrix for our deployment job. The second job, &lt;code&gt;deployment-web-app&lt;/code&gt;, depends on &lt;code&gt;determine-runners&lt;/code&gt;. It uses &lt;code&gt;fromJson(...)&lt;/code&gt; to transform the JSON string into an array for the matrix. Each array element corresponds to a runner name, resulting in one parallel job per server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  deployment-web-app:
    name: Deploy to $ on $
    environment: $
    needs: [determine-runners]
    runs-on: $
    strategy:
      fail-fast: false
      matrix:
        runner: $
    steps:
      - name: Clean Workspace Folder
        run: Remove-Item -Recurse -Force $\*

      - name: Download artifact
        uses: actions/download-artifact@v4

      # Additional steps to deploy the IIS website go here

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

&lt;/div&gt;



&lt;p&gt;In our case, deploying to IIS involved copying build artifacts, stopping the IIS site, replacing the files, and restarting the service. By running these steps on each runner in parallel, we reduced total deployment time and avoided manual coordination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;When we first set out, we worried that this dynamic approach would introduce more complexity than it solved. However, by automating the discovery of runners, we created a resilient pipeline that adapts as servers come online or go offline. Below are some key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Labels Strategically&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
By combining environment labels (e.g., &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;) with a fixed label (e.g., &lt;code&gt;IIS-Server&lt;/code&gt;), we narrowed down the runner list precisely. Labels become powerful selectors when applied consistently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manage GitHub App Credentials Carefully&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The process requires a GitHub App with appropriate permissions. You must secure its private key and ensure it remains installed on your repository. Losing this App or its credentials would break the token exchange and halt deployments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI Tools Preinstalled&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We used a GitHub-hosted runner to run this workflow, which already has &lt;code&gt;gh&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; installed by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Account for Pagination&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Our example handles only the first 100 runners. If your organization has more, implement pagination logic by iterating over pages until no runners remain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embrace Parallelism&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Running deployments in parallel saved us time and reduced the risk of inconsistent state. We could also monitor each runner’s status separately, making troubleshooting easier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;GitHub Actions does not natively support a construct like deployment groups. By querying the GitHub API, filtering runners with &lt;code&gt;jq&lt;/code&gt;, and building a dynamic matrix, we created a solution that scales automatically as servers change. Although it requires extra setup, a GitHub App, additional CLI tools, and careful label management, the result is a more flexible, resilient deployment pipeline.&lt;/p&gt;

&lt;p&gt;While this post uses IIS server deployments as the example, the same technique can be applied to any deployment scenario. By defining tags or labels on your runners and building a dynamic matrix, you can select machines based on any condition, such as environment, role, or geographic region, and run workflows in parallel across them.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>iis</category>
      <category>windows</category>
      <category>aws</category>
    </item>
    <item>
      <title>A Practical Approach to Hiring Great Full-Stack Engineers</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Fri, 27 Dec 2024 23:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/a-practical-approach-to-hiring-great-full-stack-engineers-3fg1</link>
      <guid>https://dev.to/mivano/a-practical-approach-to-hiring-great-full-stack-engineers-3fg1</guid>
      <description>&lt;p&gt;Hiring great software developers is always challenging, but the stakes are even higher when building small, versatile teams. Recently, I conducted a series of interviews for a client looking to expand their team by adding two full-stack engineers. The team setup required individuals who could work across the entire application lifecycle—designing, building, deploying, and monitoring systems—without being restricted to just frontend, backend, or DevOps.&lt;/p&gt;

&lt;p&gt;It’s no secret that finding true “full-stack” engineers is difficult. Most developers lean towards one area—be it backend, frontend, cloud, or pipelines—and that’s okay. But in this case, the organization needed engineers who understood the big picture. They had to be able to contribute meaningfully across the board in a small team setup, where there’s no room to say, “That’s someone else’s job.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process: Lengthy, But Effective
&lt;/h2&gt;

&lt;p&gt;For this reason, we committed to lengthy interviews, typically lasting 1.5 to 2 hours. It’s time-consuming, both for the candidates and for us, but it allowed us to truly assess whether someone could fit the team’s needs.&lt;/p&gt;

&lt;p&gt;The interview structure was straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Introductions&lt;/strong&gt; : We started with casual conversation—interests, hobbies, passions—to get a sense of the candidate as a person. This also helped build rapport and gave us an early impression of their communication skills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem Discussion&lt;/strong&gt; : I presented a simple but open-ended problem: designing an API to serve products. The task wasn’t about writing code; it was about having a meaningful conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Flow: From Idea to Production
&lt;/h2&gt;

&lt;p&gt;The exercise mimicked how the team worked internally: starting with a vague business problem and analyzing it to build a working system. The candidate’s job was to walk through each stage of the process with me, while I m drawing on a whiteboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Understanding the Problem&lt;/strong&gt; : Did they ask the right questions to clarify the requirements and constraints?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Design&lt;/strong&gt; : Could they explain HTTP verbs, REST principles, and how to handle authentication (e.g., what a JWT is and why it’s secure)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coding&lt;/strong&gt; : How would they implement this API? What language, framework, or libraries would they use? How would they structure the code and how to ensure quality? Did they consider testing and if so, what kind?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Considerations&lt;/strong&gt; : Did they think about indexes or the implications of their schema design?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Readiness&lt;/strong&gt; : How would they host the API? Did they mention pipelines, monitoring, or scalability? And how to get the code from their machine to production?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn’t to see how many buzzwords they could throw out or whether they knew every detail of every tool. Instead, we focused on their reasoning, communication, and ability to problem-solve collaboratively.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results: The Good, the Bad, and the Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Out of all the interviews, we only passed three candidates to the next round. Here’s what set them apart:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They Could Have a Conversation&lt;/strong&gt; : These candidates were curious, asked great questions, and actively participated in the discussion. Even when they didn’t know everything, they admitted it honestly and showed an understanding of the broader picture. For example, they recognized the need for hosting, securing, and monitoring the API—even if they weren’t experts in every tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They Thought Beyond the Immediate Problem&lt;/strong&gt; : One standout candidate designed the system with cost optimization and scalability in mind, leveraging PaaS solutions to keep it efficient. This kind of forward-thinking demonstrated a level of maturity and ownership we rarely see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They Showed Accountability&lt;/strong&gt; : In small teams, everyone needs to pitch in across the lifecycle. These candidates understood the importance of being able to handle a variety of tasks. By contrast, many other candidates relied too heavily on the idea of separate teams handling pipelines, quality control, or rollouts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unfortunately, we also encountered several candidates with impressive CVs who struggled to articulate their understanding of the concepts they claimed to know. Some couldn’t design any part of the system or explain the reasoning behind their choices. Others had limited exposure to the end-to-end development lifecycle because they worked in large organizations where tasks were heavily siloed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway: Real Conversations Trump CVs
&lt;/h2&gt;

&lt;p&gt;While resumes and buzzwords can get candidates through the door, they rarely tell the full story. A collaborative, problem-solving interview process, though exhausting, gave us the confidence to hire engineers who could truly add value to the team.&lt;/p&gt;

&lt;p&gt;For the candidates, the process also served as a preview of the company’s expectations. By focusing on real-world problems and reasoning, we ensured alignment between the team’s needs and the candidate’s strengths.&lt;/p&gt;

&lt;p&gt;Ultimately, hiring isn’t just about filling a role—it’s about finding the right person who can thrive in the unique environment of your organization.&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>fullstack</category>
      <category>engineering</category>
      <category>interview</category>
    </item>
    <item>
      <title>Streamlining Helpdesk Operations: Leveraging GitHub Issues with Custom Workflows and Templates</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Mon, 01 Jan 2024 23:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/streamlining-helpdesk-operations-leveraging-github-issues-with-custom-workflows-and-templates-145e</link>
      <guid>https://dev.to/mivano/streamlining-helpdesk-operations-leveraging-github-issues-with-custom-workflows-and-templates-145e</guid>
      <description>&lt;h2&gt;
  
  
  Setting the Scene: Choosing GitHub Issues for Internal Helpdesk Needs.
&lt;/h2&gt;

&lt;p&gt;In the realm of helpdesk and ticketing systems, the market is brimming with options. From Zohodesk and Freshdesk to Zendesk and Jira, the choices are many, some even offering enticing free entry points. However, a common catch with these platforms is the pricing model, which typically scales based on the number of agents, leading to a significant cost increase over time. This challenge became evident in a recent customer project I was involved in. While we successfully implemented Freshdesk as the support system for the customer’s SaaS application, our internal requirements painted a different picture.&lt;/p&gt;

&lt;p&gt;Our needs revolved around internal operations like account management, onboarding/offboarding procedures, addressing hardware issues, and managing licenses—tasks distinctly separate from customer interactions and not fitting neatly into the development team’s responsibilities. Adding to the complexity was the customer’s ISO certification, necessitating meticulous tracking of changes and actions. This scenario required a separate system from the customer-facing helpdesk, yet something that was both minimalistic and easy to manage.&lt;/p&gt;

&lt;p&gt;The solution lay closer than we thought. With all team members already well-versed in GitHub, leveraging its environment was a logical step. GitHub Issues, when combined with customized workflows and templates, presented a unique opportunity. It offered a familiar platform that was both cost-effective and adaptable to our specific internal needs. In this post, I’ll guide you through the process of setting up GitHub Issues as an efficient, internal helpdesk system, elucidating how it can be tailored to streamline your organizational processes, just as it did for ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying the Foundations: Initial Setup of Your GitHub Helpdesk
&lt;/h2&gt;

&lt;p&gt;In the journey of transforming GitHub into an effective internal helpdesk, the initial setup plays a crucial role. GitHub offers two distinct systems for tracking activities: Issues and Discussions. Understanding the nuances of each is key to leveraging them effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issues vs. Discussions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issues&lt;/strong&gt; : This feature in GitHub is straightforward. Each issue comprises a title, a descriptive body, optional labels, and assignments. It’s the go-to choice for tracking specific tasks, bugs, or requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions&lt;/strong&gt; : In contrast, Discussions provide a forum-like environment. They’re ideal for cases where you want to engage in a broader conversation before possibly converting the discussion into a more formal issue. Not everything that crops up is immediately an issue, and Discussions provide the flexibility to explore topics more broadly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the purpose of an internal helpdesk, where submissions are likely to be direct issues, focusing on the Issues feature is the most practical approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your Repository
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a New Repository&lt;/strong&gt; : Start with a fresh slate by setting up a new repository within your organization. A simple and descriptive name like ‘servicedesk’ or ‘helpdesk’ works well. Remember to keep it internal/private to maintain confidentiality. &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4FimmDpT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/create-helpdesk-repo.png" alt="Create Repository" width="800" height="769"&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Craft Your README.md&lt;/strong&gt; : The &lt;code&gt;README.md&lt;/code&gt; file is the first point of contact for users visiting your repository. It’s a Markdown file that can be used to provide essential instructions and guidelines. Here’s how you can structure it:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Introduction&lt;/em&gt;: Briefly describe the purpose of the helpdesk and what types of issues should be submitted here.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How to Submit an Issue&lt;/em&gt;: Give clear, step-by-step instructions on how to create a new issue, including guidance on writing a good title and description.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Label Usage&lt;/em&gt;: Explain how labels are used within the repository to categorize and prioritize issues.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Response Times and Process&lt;/em&gt;: Outline what users can expect in terms of response times and the process their issues will go through.&lt;/p&gt;

&lt;p&gt;For example, here’s a sample README.md file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Service Desk

Service Desk system for X.

## What is this?

This repository can be used to record issue requests to the service organisation operated by Y. Like requesting access to or gaining new accounts, having issues with hardware, or have problems with your workstation or phone.

This is not for software production issues; that is for the devteam, reachable in [Slack](link to slack).

Customers having questions or problems with the usage of Z, will need to go to [Freshdesk](link to freshdesk).

## How does it work?

When you want to register an item, create a [new issue](https://github.com/yourorg/yourrepo/issues/new/choose) and use the most applicable template. 

The issue will be automatically assigned to the service team at Y and they will react via the issue. 

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

&lt;/div&gt;



&lt;p&gt;Creating this foundational setup ensures a streamlined experience for both those managing the helpdesk and those using it, setting the stage for efficient internal support management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing Issue Reporting: Implementing Issue Templates
&lt;/h2&gt;

&lt;p&gt;A common challenge in issue tracking is receiving submissions that lack essential information. This often leads to a back-and-forth to gather the necessary details, causing delays and inefficiencies. To address this, GitHub offers a powerful tool: issue templates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Issue Templates
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Template Files&lt;/strong&gt; : Start by creating your issue templates. These templates are files that reside in the &lt;code&gt;.github/ISSUE_TEMPLATE&lt;/code&gt; folder of your repository. You can create multiple files here to cater to different types of requests or issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design Your Templates&lt;/strong&gt; : Each template should be designed to collect specific data pertinent to the type of issue it represents. This structured approach ensures that when a user submits an issue, they provide all the necessary information upfront.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An example of a template for requesting a new account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Accounts
description: Onboarding or offboarding, getting access to a system
title: "[Account]: "
labels: ["account", "triage"]
assignees:
  - userx
  - usery
body:
  - type: markdown
    attributes:
      value: |
        Use this template to request access to a system, onboard or offboard an employee or ask for a change in permissions. 
  - type: input
    id: person
    attributes:
      label: Contact Details
      description: For who is the change intended?
      placeholder: ex. email@example.net
    validations:
      required: true
  - type: textarea
    id: what-need-to-change
    attributes:
      label: What need to change?
      description: What is the action that needs to be taken?
      placeholder: Tell us what needs to be done
      value: "This person need access to..."
    validations:
      required: true
  - type: input
    id: when
    attributes:
      label: When
      description: When does it need to be done?
      placeholder: Leave empty for ASAP or specify a date
    validations:
      required: false


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

&lt;/div&gt;



&lt;p&gt;Or for hardware issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Hardware
description: Issues with hardware, like replacing, defects
title: "[Hardware]: "
labels: ["hardware", "triage"]
assignees:
  - userx
  - usery
body:
  - type: markdown
    attributes:
      value: |
        Use this template to request support on anykind of hardware you have; broken phone, PC not working, lost mouse, use this to get us involved.
  - type: input
    id: person
    attributes:
      label: Contact Details
      description: Who has issues with the hardware
      placeholder: ex. email@example.net
    validations:
      required: true

  - type: dropdown
    id: area
    attributes:
      label: Area
      options:
        - Desktop
        - Phone  
      description: Indicate which area is impacted 
  - type: textarea
    id: what-need-to-change
    attributes:
      label: What need to change?
      description: What is the action that needs to be taken?
      placeholder: Tell us what needs to be done
      value: "This piece of equipement is no longer working..."
    validations:
      required: true    
  - type: input
    id: when
    attributes:
      label: When
      description: When does it need to be done?
      placeholder: Leave empty for ASAP or specify a date
    validations:
      required: false

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuring the Template Selector&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a config.yml File&lt;/strong&gt; : This file goes in the same &lt;code&gt;.github/ISSUE_TEMPLATE&lt;/code&gt; folder. It’s used to configure how users select an issue template. For example, here’s a sample config.yml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blank_issues_enabled: true
contact_links:
  - name: Freshdesk
    url: https://support.x.nl/a/dashboard/default
    about: For customer issues.

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration Options&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;blank_issues_enabled: true&lt;/code&gt; - This setting allows users to still create blank issues. However, disabling this feature means users can only use your predefined templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact Links&lt;/strong&gt; : These are additional options you can provide for users. For example: 

&lt;ul&gt;
&lt;li&gt;Freshdesk for customer issues with a direct link to your Freshdesk dashboard.&lt;/li&gt;
&lt;li&gt;Slack for devteam issues with a direct link to your Slack workspace.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These links not only guide users to the right resources but also help in segregating different types of issues efficiently.&lt;/p&gt;

&lt;p&gt;By implementing these templates and configuration settings, you streamline the issue submission process. Users are guided to provide all necessary information, reducing the need for follow-up and speeding up the resolution process. To direct users to the template selector, use a URL like &lt;code&gt;https://github.com/yourorg/yourrepo/issues/new/choose&lt;/code&gt;. This ensures a more organized and efficient approach to managing internal helpdesk requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NE7QmVSl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/select-issue-template.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NE7QmVSl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/select-issue-template.png" alt="Select an issue template" width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective Issue Management: Workflows and Automation
&lt;/h2&gt;

&lt;p&gt;As the number of issues in your GitHub helpdesk repository grows, effective management and maintenance become crucial. To ensure efficiency and order, we utilize a combination of GitHub workflows and Probot actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing Empty Issues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Request-Info Bot&lt;/strong&gt; : One of the common frustrations in issue tracking is receiving issues with a title but no descriptive content. To address this, we implement the Request-Info bot (&lt;a href="https://probot.github.io/apps/request-info/"&gt;Request Info Probot&lt;/a&gt;). This bot is configured to prompt for more information on such issues. Install it in your helpdesk repository and create a &lt;code&gt;.github/config.yml&lt;/code&gt; file with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Configuration for request-info - https://github.com/behaviorbot/request-info

# *Required* Comment to reply with
requestInfoReplyComment: &amp;gt;
  We would appreciate it if you could provide us with more info about this issue!

# *OPTIONAL* default titles to check against for lack of descriptiveness
# MUST BE ALL LOWERCASE
requestInfoDefaultTitles:
  - update readme.md
  - updates

# *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given
requestInfoLabelToAdd: needs-more-info

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automated Assignment of Issues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Auto-Assign Issue Action&lt;/strong&gt; : To ensure issues are promptly addressed, we use the &lt;a href="https://github.com/marketplace/actions/auto-assign-issue"&gt;Auto-Assign Issue Action&lt;/a&gt;. This action automatically assigns new issues to the responsible team members. While issue templates allow for assigning users, this action covers scenarios where blank issues are created.&lt;/p&gt;

&lt;h3&gt;
  
  
  Labeling for Triage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Triage-New-Issues App&lt;/strong&gt; : Apply a ‘Triage’ label to new issues using the &lt;a href="https://github.com/apps/triage-new-issues"&gt;Triage New Issues App&lt;/a&gt;. This helps in quickly identifying and categorizing new submissions for further action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Stale Issues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stale Issue Workflow&lt;/strong&gt; : Keeping issues open for too long without activity can clutter your helpdesk. To manage this, we implement a workflow to mark stale issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   name: 'Close stale issues and PRs'
   on:
     schedule:
       - cron: '30 1 * * *'

   jobs:
     stale:
       runs-on: ubuntu-latest
       steps:
         - uses: actions/stale@v9
           with:
             stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
             days-before-stale: 30
             days-before-close: 5

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

&lt;/div&gt;



&lt;p&gt;This workflow runs daily, identifying issues open for more than 30 days and marking them as stale. This process helps maintain a cleaner, more manageable issue list and ensures issues don’t linger unresolved.&lt;/p&gt;

&lt;p&gt;By integrating these tools and workflows, you create a dynamic and responsive helpdesk system within GitHub, capable of handling issues efficiently and ensuring nothing falls through the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harnessing Data: Implementing Analytics in Your GitHub Helpdesk
&lt;/h2&gt;

&lt;p&gt;One of the advantages of professional helpdesk systems is their capability to provide analytics, such as resolution times and the duration for which issues remain open. While GitHub might not offer these features out-of-the-box, we can achieve similar analytics through a custom workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Monthly Issue Metrics
&lt;/h3&gt;

&lt;p&gt;This workflow is designed to run monthly and collect data on issues created and resolved within that period, providing valuable insights into the performance of your helpdesk system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow Configuration&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Monthly issue metrics
on:
  workflow_dispatch:
  schedule:
    - cron: '3 2 1 * *'

jobs:
  build:
    name: issue metrics
    runs-on: ubuntu-latest
    permissions:
      actions: read
      issues: write
    steps:

    - name: Get dates for last month
      shell: bash
      run: |
        # Script to calculate date range of the previous month
        [Date calculation script here]

    - name: Run issue-metrics tool
      uses: github/issue-metrics@v2
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SEARCH_QUERY: 'repo:yourrepo/servicedesk is:issue created:${{ env.last_month }} -reason:"not planned"'

    - name: Create issue
      uses: peter-evans/create-issue-from-file@v4
      with:
        title: Monthly issue metrics report
        content-filepath: ./issue_metrics.md
        assignees: mivano

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Components&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date Calculation&lt;/strong&gt; : The workflow begins by calculating the date range for the previous month. This range is used to filter the issues for the metrics report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue Metrics Tool&lt;/strong&gt; : Utilizing the ‘github/issue-metrics@v2’ action, the workflow gathers data on issues created within the specified date range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report Creation&lt;/strong&gt; : An issue is automatically created containing the metrics, which can then be labeled, assigned, and reviewed like any other issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This workflow transforms GitHub into a more robust helpdesk tool, providing monthly analytics similar to those offered by specialized helpdesk systems. The generated report gives a clear picture of helpdesk performance, helping identify areas for improvement. More information on this action and its capabilities can be found on the &lt;a href="https://github.blog/2023-07-19-metrics-for-issues-pull-requests-and-discussions/"&gt;GitHub Blog&lt;/a&gt;. By incorporating this workflow, you enhance the functionality of your GitHub helpdesk, leveraging data to drive efficiency and effectiveness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating Recurring Tasks: Workflow for Scheduled Issues
&lt;/h2&gt;

&lt;p&gt;In the management of any helpdesk or support system, certain tasks are bound to recur regularly. These can range from routine checks to periodic backups. To handle such recurring issues efficiently within GitHub, we can leverage the power of automated workflows. This approach ensures that these tasks are consistently addressed without fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Workflow for Recurring Issues
&lt;/h3&gt;

&lt;p&gt;Here’s an example of a workflow designed to create a new issue for quarterly checks of users and authorizations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow Definition&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Quarterly check users and authorisations
on:
  workflow_dispatch:    
  schedule:
    - cron: 0 12 1 */3 *

jobs:
  create_issue:
    name: Create issue - Quarterly check users and authorisations
    runs-on: ubuntu-latest
    steps:
      - name: Create issue 
        run: |
          new_issue_url=$(gh issue create \
            --title "$TITLE" \
            --label "$LABELS" \
            --body "$BODY" \
            --assignee "$ASSIGNEE")
        env:
          GITHUB_TOKEN: $
          GH_REPO: $
          TITLE: Quarterly check users and authorisations
          LABELS: iso
          ASSIGNEE: userX,userY
          BODY: |
            Perform the quarterly checks for users and the authorisations. See the ISMS for more details.
          PINNED: false

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Components&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cron Schedule&lt;/strong&gt; : Set to trigger quarterly (&lt;code&gt;0 12 1 */3 *&lt;/code&gt;). This timing can be adjusted based on the frequency needed for the particular task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue Creation Step&lt;/strong&gt; : Automates the creation of a new GitHub issue with specific details like title, labels, body content, and assignees.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Expanding the Workflow
&lt;/h3&gt;

&lt;p&gt;The beauty of this system lies in its flexibility. By modifying the trigger and the content of the issue, and saving it as a new file in the &lt;code&gt;.github/workflows&lt;/code&gt; folder, you can create workflows for various other recurring issues. This method is not only efficient but also ensures consistency and reliability in performing routine tasks that are crucial for your organization.&lt;/p&gt;

&lt;p&gt;Implementing such automated workflows for recurring issues in your GitHub helpdesk repository aids in maintaining regularity and ensures that important tasks are not overlooked. This level of automation brings a new dimension of efficiency to your internal helpdesk operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: GitHub as an Ideal Internal Helpdesk Solution
&lt;/h2&gt;

&lt;p&gt;In conclusion, leveraging GitHub for your internal helpdesk system offers numerous advantages, especially when considering the ecosystem and tooling already familiar to many teams. This approach not only streamlines internal support processes but also adds a layer of efficiency and integration that traditional helpdesk tools may not provide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Advantages of Using GitHub for Internal Helpdesk:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unified Ecosystem&lt;/strong&gt; : Staying within GitHub means no need to juggle between different platforms, reducing the learning curve and integration complexities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost-Effective&lt;/strong&gt; : Eliminates the additional costs associated with third-party helpdesk tools, as GitHub already forms part of many organizations’ toolsets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Robust Issue Tracking Features&lt;/strong&gt; : Utilize GitHub’s native issue features like templates, formatting options, attachments, mentions, and cross-referencing, enhancing communication and tracking efficiency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customizable Workflows and Apps&lt;/strong&gt; : The ability to add workflows and apps further tailors the experience to your organization’s specific needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Project Boards for Visualization&lt;/strong&gt; : Employ GitHub’s project boards for a visual representation of tasks, allowing you to effortlessly manage and move items through to completion.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  External Use Considerations:
&lt;/h3&gt;

&lt;p&gt;While GitHub excels for internal use, there are challenges when considering external helpdesk applications. Non-GitHub users cannot create issues, and external access to a private helpdesk repository is not advisable. Additionally, GitHub doesn’t natively handle email-based ticket creation or manage attachments from external sources effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution for External Use: Scitor.io
&lt;/h3&gt;

&lt;p&gt;For those seeking to extend GitHub’s capabilities to an external audience, &lt;a href="https://www.scitor.io"&gt;Scitor.io&lt;/a&gt; offers a solution. It transforms GitHub into a more traditional helpdesk system, bridging the gap for external user interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;For internal team use, GitHub stands out as a practical, cost-effective, and efficient tool for helpdesk management. It consolidates various aspects of issue tracking and resolution into a single, integrated platform, familiar to most developers and IT professionals. By following the outlined steps and considerations, you can effectively transform GitHub into a powerful tool for managing your internal helpdesk needs, harnessing its full potential to streamline and enhance your support processes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@cdc?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;CDC&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/man-in-black-and-white-checkered-dress-shirt-using-computer-_XLJy3h77cw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
      <category>helpdesk</category>
    </item>
    <item>
      <title>Apply caching when restoring NuGet packages using a GitHub hosted runner</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Thu, 21 Sep 2023 22:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/apply-caching-when-restoring-nuget-packages-using-a-github-hosted-runner-31l4</link>
      <guid>https://dev.to/mivano/apply-caching-when-restoring-nuget-packages-using-a-github-hosted-runner-31l4</guid>
      <description>&lt;p&gt;When you are using a GitHub hosted runner to build your project, you can apply caching to speed up the build process. This is especially useful when you are using NuGet packages. As you get a new runner every time you run a build, you will need to restore all NuGet packages every time. This can take a lot of time as it is not the most efficient network calls.&lt;/p&gt;

&lt;p&gt;To speed up the process, you can cache the NuGet packages. This will make sure that the next time you run a build, the NuGet packages will be restored from the cache instead of the NuGet website. To make sure that the cache is invalidated when a new version of a NuGet package is referenced, you can use the hash of the project files as part of the cache key. This will make sure that the cache is invalidated when changes are made to the project files.&lt;/p&gt;

&lt;p&gt;For completness sake, I have included a full example of a workflow file that uses caching to speed up the build process. GitHub will automatically add a post step to store the files in the cache.&lt;/p&gt;

&lt;p&gt;By setting the &lt;code&gt;NUGET_PACKAGES&lt;/code&gt; environment variable, you can make sure that the NuGet packages are restored to the same location as the cache. This will make sure that the cache is used when restoring the NuGet packages.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Services&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&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;Build services&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;NUGET_PACKAGES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$/.nuget/packages&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&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;Setup .NET&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;actions/setup-dotnet@v3&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;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7.0.x&lt;/span&gt;

      &lt;span class="pi"&gt;-&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;actions/cache@v3&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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$\.nuget\packages&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$-nuget-$&lt;/span&gt; &lt;span class="c1"&gt;#hash of project files&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;$-nuget-&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;Restore dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet restore solution.sln&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;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build solution.sln --no-restore&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;Test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet test solution.sln --no-build --verbosity normal&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;Publish&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt; 
          &lt;span class="s"&gt;dotnet publish solution.sln -c Release --no-restore -o publish &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;Upload a Build Artifact&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;actions/upload-artifact@v3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;services&lt;/span&gt;
          &lt;span class="na"&gt;path&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;if-no-files-found&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; 

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

&lt;/div&gt;



&lt;p&gt;This workflow will restore, build, test and package the solution. The resulting package will be uploaded as an artifact and can be deployed in subsequent steps.&lt;/p&gt;

&lt;p&gt;Do use the logs to verify that the cache is used and validate if the amount of time that is used is worthwhile.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
    </item>
    <item>
      <title>Sending Emails On Behalf of Someone Else in SaaS Solutions Using SendGrid and .NET</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Mon, 28 Aug 2023 22:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/sending-emails-on-behalf-of-someone-else-in-saas-solutions-using-sendgrid-and-net-5b2b</link>
      <guid>https://dev.to/mivano/sending-emails-on-behalf-of-someone-else-in-saas-solutions-using-sendgrid-and-net-5b2b</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the modern SaaS landscape, flexibility is key. One commonly sought-after feature in Helpdesk and similar software is the ability to send emails on behalf of a different domain—your customer’s domain, for instance. However, spam prevention and domain authentication can make this tricky. If you’re building a SaaS solution that deals with email services, like my current project &lt;a href="https://www.scitor.io"&gt;Scitor&lt;/a&gt;, this blog post is for you. In it, I’ll explain how I implemented a secure way to send emails from my customer’s domain, using SendGrid and .NET.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When implementing &lt;a href="https://www.scitor.io"&gt;Scitor&lt;/a&gt;, a Helpdesk SaaS that accepts emails and converts them into GitHub discussions, I ran into a challenge. The solution needed to send replies back to the original recipient’s email address, and many customers wanted those emails to come from their own domain.&lt;/p&gt;

&lt;p&gt;However, due to spam prevention measures, you can’t simply send emails from a domain you don’t own or haven’t authenticated. This is not only bad for your spam reputation; SendGrid explicitly forbids it unless the ‘from’ email address has been verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Domain Authentication
&lt;/h2&gt;

&lt;p&gt;I found a way to allow customers to authenticate their domain without giving them access to my SendGrid account. I also did not have a GUI or admin interface for my solution, so I needed to automate the process as much as possible within the GitHub discussion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The &lt;code&gt;/authenticate-domain&lt;/code&gt; Command
&lt;/h3&gt;

&lt;p&gt;The first step is to create a command—&lt;code&gt;/authenticate-domain&lt;/code&gt; that customers can use to begin the domain authentication process. When a customer issues this command by adding it to a comment in the discussions, the following code creates a domain registration at SendGrid via the StrongGrid library:&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;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;DomainRegistrationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateDomain&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;domain&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;result&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;_strongGridClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SenderAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDomainAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&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;new&lt;/span&gt; &lt;span class="nf"&gt;DomainRegistrationResult&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;Id&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;IsValid&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;DNSRecordResult&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim1&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&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;DNSRecordResult&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim2&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&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;DNSRecordResult&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MailCName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&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;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MailCName&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MailCName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&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;record&lt;/span&gt; &lt;span class="nc"&gt;DomainRegistrationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;DomainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordResult&lt;/span&gt; &lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordResult&lt;/span&gt; &lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordResult&lt;/span&gt; &lt;span class="n"&gt;CName&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;record&lt;/span&gt; &lt;span class="nc"&gt;DNSRecordResult&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;Data&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;Type&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;Host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The function returns DNS records that the customer needs to configure on their end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configuring DNS Settings
&lt;/h3&gt;

&lt;p&gt;I output these DNS settings into the GitHub discussion as an additional comment. The customer is then responsible for adding these DNS records, a process that can take some time and differ per DNS provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ahn4H5g6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/domain-auth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ahn4H5g6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/domain-auth.png" alt="Domain Authentication" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The &lt;code&gt;/verify-domain&lt;/code&gt; Command
&lt;/h3&gt;

&lt;p&gt;Once the customer has configured their DNS settings, they issue the &lt;code&gt;/verify-domain&lt;/code&gt; command. This function verifies the domain using SendGrid’s API:&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;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;DomainValidationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;VerifyDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;domainId&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;result&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;_strongGridClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SenderAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDomainAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domainId&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;new&lt;/span&gt; &lt;span class="nf"&gt;DomainValidationResult&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;DomainId&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;IsValid&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;DNSRecordValidationResult&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&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;DNSRecordValidationResult&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&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;DNSRecordValidationResult&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&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;ValidationResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&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;record&lt;/span&gt; &lt;span class="nc"&gt;DomainValidationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;DomainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordValidationResult&lt;/span&gt; &lt;span class="n"&gt;Dkim1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordValidationResult&lt;/span&gt; &lt;span class="n"&gt;Dkim2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DNSRecordValidationResult&lt;/span&gt; &lt;span class="n"&gt;CName&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;record&lt;/span&gt; &lt;span class="nc"&gt;DNSRecordValidationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsValid&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;Reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;When all records are validated successfully, the customer can start using their own domain as the sender. If not, I list the problems that still need to be fixed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AtGG1aRT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/domain-verify.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AtGG1aRT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.mindbyte.nl/images/domain-verify.png" alt="Domain Validation" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I do store the returned &lt;code&gt;domainId&lt;/code&gt; in my database, so I can use it to remove the domain later if needed and check if the verification has been completed or not. There is also an explicit &lt;code&gt;/delete-domain&lt;/code&gt; command that customers can use to remove the domain from my SendGrid account.&lt;/p&gt;

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

&lt;p&gt;Domain authentication may sound complex, but with a bit of code and the help of SendGrid’s StrongGrid library, it can be automated to a large extent, even for customers who don’t have direct access to your SendGrid account. This ensures that your SaaS can send emails from different domains while maintaining good spam reputation and adhering to SendGrid’s rules.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>coding</category>
    </item>
    <item>
      <title>Making API calls more resilient</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Sun, 27 Aug 2023 22:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/making-api-calls-more-resilient-1i16</link>
      <guid>https://dev.to/mivano/making-api-calls-more-resilient-1i16</guid>
      <description>&lt;p&gt;In the world of networked applications, call failures are a common occurrence due to the inherent unreliability of networks. When working with the Azure Cost API, challenges such as indefinite retries, excessive wait times, and rate limitations can impede efficiency and efficacy. This post explores a method to make API calls more resilient, taking into account these critical factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unreliable Network and Rate Limitations
&lt;/h2&gt;

&lt;p&gt;When making requests to an API, calls can fail for various reasons ranging from network unreliability to server-side rate limits. Repeated retries, lengthy waiting periods, or disregard for server-specified limitations can lead to inefficiencies or even service denial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polly - .NET Resilience Library
&lt;/h2&gt;

&lt;p&gt;To address these challenges, &lt;a href="https://www.thepollyproject.org"&gt;Polly&lt;/a&gt;, a .NET resilience and transient-fault-handling library, was employed. It enables developers to define policies like Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback, thereby increasing the resilience of API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining a Resilient Policy with Polly
&lt;/h2&gt;

&lt;p&gt;The core of this approach lies in defining a resilient policy that combines a wait-and-retry policy with a timeout policy. Below is the code snippet that showcases this process:&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PollyPolicyExtensions&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="n"&gt;IAsyncPolicy&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;GetRetryAfterPolicy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Define WaitAndRetry policy&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;waitAndRetryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleResult&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="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
                &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TooManyRequests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAndRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;sleepDurationProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&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;_&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;headers&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;Headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&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;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;header&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&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;ToLower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"retry-after"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;seconds&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;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="c1"&gt;// If no header with a retry-after value is found, fall back to 2 seconds.&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;onRetryAsync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Define Timeout policy&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timeoutPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutAsync&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="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Wrap WaitAndRetry with Timeout&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resilientPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WrapAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waitAndRetryPolicy&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;resilientPolicy&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;This policy intelligently handles HTTP 429 (Too Many Requests) responses by parsing the “retry-after” header and waiting for the specified duration before retrying, up to five times. If the header isn’t found, it falls back to a two-second wait time. Additionally, a timeout policy ensures that calls do not wait longer than 60 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the Policy
&lt;/h2&gt;

&lt;p&gt;The defined policy is then applied to the HTTP client, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registrations.AddHttpClient("CostApi", client =&amp;gt;
{
  client.BaseAddress = new Uri("https://management.azure.com/");
  client.DefaultRequestHeaders.Add("Accept", "application/json");
}).AddPolicyHandler(PollyPolicyExtensions.GetRetryAfterPolicy());

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

&lt;/div&gt;



&lt;p&gt;This registration is placed in the &lt;code&gt;ConfigureServices&lt;/code&gt; method of the &lt;code&gt;Startup&lt;/code&gt; class, ensuring that the policy is applied to all HTTP calls made by the application when they request this &lt;code&gt;CostApi&lt;/code&gt; httpclient from the factory.&lt;/p&gt;

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

&lt;p&gt;The combination of Polly’s features offers a robust and scalable solution to the common challenges faced when making resilient API calls. By leveraging this method, developers can create a more stable and efficient communication with the Azure Cost API, providing a seamless experience even in unpredictable network conditions.&lt;/p&gt;

&lt;p&gt;For those looking to enhance their API interactions, exploring Polly and the resilient policies it supports can be a game-changer. The flexibility and reliability it offers make it an essential tool for modern developers navigating the complexities of network communication.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>rest</category>
      <category>api</category>
    </item>
    <item>
      <title>Retrieving the active Azure Subscription ID from the AZ CLI Context</title>
      <dc:creator>Michiel van Oudheusden</dc:creator>
      <pubDate>Fri, 25 Aug 2023 22:00:00 +0000</pubDate>
      <link>https://dev.to/mivano/retrieving-the-active-azure-subscription-id-from-the-az-cli-context-l3</link>
      <guid>https://dev.to/mivano/retrieving-the-active-azure-subscription-id-from-the-az-cli-context-l3</guid>
      <description>&lt;p&gt;Managing Azure subscriptions can be a delicate task, especially when dealing with various tools and applications that require context-specific information. In building the &lt;a href="https://github.com/mivano/azure-cost-cli"&gt;Azure Cost CLI&lt;/a&gt; a requirement arose to determine the current active subscription ID when it’s not explicitly passed in. This post will dive into the process of retrieving this information, similar to the Azure CLI’s default behavior set by &lt;code&gt;az account set -s&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default Subscription ID
&lt;/h2&gt;

&lt;p&gt;The Azure CLI allows users to set a default subscription ID, and mimicking this behavior in the Azure Cost CLI tool brings in alignment and familiarity. The question was, how could this be achieved programmatically?&lt;/p&gt;

&lt;h2&gt;
  
  
  Executing the AZ Command
&lt;/h2&gt;

&lt;p&gt;The solution lay in executing the Azure CLI’s &lt;code&gt;az account show&lt;/code&gt; command, which returns information about the current account, including the active subscription ID.&lt;/p&gt;

&lt;p&gt;The following C# code is used to execute the command and extract the subscription ID:&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AzCommand&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetDefaultAzureSubscriptionId&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;startInfo&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;ProcessStartInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;FileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"az"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"account show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;RedirectStandardOutput&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="n"&gt;RedirectStandardError&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="n"&gt;UseShellExecute&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="n"&gt;CreateNoWindow&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="k"&gt;using&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;process&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;Process&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;StartInfo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startInfo&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&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;output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadToEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForExit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExitCode&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadToEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Error executing 'az account show': &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&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;using&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;jsonDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;idElement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;subscriptionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&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;subscriptionId&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to find the 'id' property in the JSON output."&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here’s what the code does, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Creating Process Start Information&lt;/strong&gt; : A &lt;code&gt;ProcessStartInfo&lt;/code&gt; object is initialized with the required command and settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executing the Process&lt;/strong&gt; : A new process is started to execute the command, and the standard output is read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Handling&lt;/strong&gt; : If the command execution fails, an error is thrown with the details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing the Output&lt;/strong&gt; : The JSON output is parsed to retrieve the &lt;code&gt;id&lt;/code&gt; property, which holds the subscription ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Seamless Integration
&lt;/h2&gt;

&lt;p&gt;The above code snippet integrates seamlessly into the Azure Cost CLI tool, providing an efficient and consistent way to retrieve the current active Azure subscription ID. By leveraging existing CLI commands and C# process management, developers can easily obtain context-specific information.&lt;/p&gt;

&lt;p&gt;This example further illustrates how familiar tools and libraries can be used to build sophisticated features, bridging gaps between different systems and providing a cohesive user experience.&lt;/p&gt;

</description>
      <category>azure</category>
    </item>
  </channel>
</rss>
