<?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: Steven Davies</title>
    <description>The latest articles on DEV Community by Steven Davies (@nullabletype).</description>
    <link>https://dev.to/nullabletype</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F701664%2F3062b681-2626-44b2-b72e-9b5b8932e47b.png</url>
      <title>DEV Community: Steven Davies</title>
      <link>https://dev.to/nullabletype</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nullabletype"/>
    <language>en</language>
    <item>
      <title>Building an agentic workflow, part 2: testing what done means</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Tue, 27 Oct 2026 00:08:40 +0000</pubDate>
      <link>https://dev.to/nullabletype/building-an-agentic-workflow-part-2-testing-what-done-means-2ikj</link>
      <guid>https://dev.to/nullabletype/building-an-agentic-workflow-part-2-testing-what-done-means-2ikj</guid>
      <description>&lt;p&gt;At the end of part 1, I had a specification, prototype tickets, and rules for how an agent should work through them. There was no application or controller yet. The next step was to try those rules against something that could actually fail.&lt;/p&gt;

&lt;p&gt;The dot-finance prototypes gave me a desktop application to use, encrypted storage and backup experiments, and a small controller to manage the agent's work and stop it when it reached the agreed limits. They also gave me a test that passed for the wrong reason, an interface that needed several rounds of correction, and a build pipeline that ran out of artifact storage.&lt;/p&gt;

&lt;p&gt;By the end of this stage, I had a good base for production development to start. Getting there involved looking quite closely at what the passing checks established, and what they hadn't tested at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interruption test hadn't interrupted anything
&lt;/h2&gt;

&lt;p&gt;The persistence prototype could save and reopen synthetic data, restore backups, and reject the wrong-password and damaged-file cases covered by the tests. Independent agent review then found a gap in the interruption coverage.&lt;/p&gt;

&lt;p&gt;Asking an operation to cancel gives it an opportunity to clean up. Killing the process during a backup doesn't. I needed to know what happened in that second condition.&lt;/p&gt;

&lt;p&gt;The agent added a harness that launched a child process and attempted to kill it at a controlled checkpoint. Initially, the harness accepted any unsuccessful exit as evidence that the interruption had happened. Unfortunately, the process-kill call itself threw an exception.&lt;/p&gt;

&lt;p&gt;The test appeared to pass because something had failed. It hadn't reached the failure I wanted to test.&lt;/p&gt;

&lt;p&gt;The missing partial backup file helped expose the mistake. Fixing the kill call dealt with that immediate problem, but a further review required an explicit marker showing that the process had reached the intended checkpoint. The checks then had to establish that recovery worked and the previous completed backup was unchanged.&lt;/p&gt;

&lt;p&gt;This was a useful example of why independent review in the loop is so important. The code and its test could agree with each other while missing the requirement. A reviewer asking what had actually happened exposed a much weaker check than the result suggested. The same reason I'd ask a colleague for a second pair of eyes when I've been deep in the weeds.&lt;/p&gt;

&lt;p&gt;The corrected test still had a specific scope. It demonstrated recovery from interruption at a controlled checkpoint, not at every possible instant during a write. Separate agent review wasn't a specialist security audit either. That gave me more confidence in the backup handling, but there was still plenty it hadn't tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The move from mock-up to actual app was rough
&lt;/h2&gt;

&lt;p&gt;I'd chosen a specific UI direction from the mockups, but the first workbench implementation had strayed significantly from it. It'll be interesting to investigate this more in the future, as I think this may be something more common to less used frameworks like Avalonia over say HTML or Typescript. Either way, this needed to be addressed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h50jm640gjugpkj9buc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h50jm640gjugpkj9buc.png" alt="The first workbench implementation, with a permanent password field in the header, small text and transaction content intruding into neighbouring columns." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first workbench implementation I sent back with feedback. The data is fictional.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The feedback was ordinary desktop application feedback. Text was too small, icons were ambiguous or missing, columns didn't line up properly, and the scrollbar obscured the amount column. The password field had become a permanent fixture in the header, when it belonged with saving and reopening the data.&lt;/p&gt;

&lt;p&gt;I also needed category totals visible while reviewing transactions. A final confirmation screen wouldn't do the same job. If I'm changing an allocation, I need to see its effect alongside the transaction I'm changing. That became an explicit specification and acceptance requirement.&lt;/p&gt;

&lt;p&gt;The corrections brought in recognisable outline icons, larger text, clearer column boundaries, and password prompts attached to the operations that needed them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqj24pfqr98o0mqchrkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqj24pfqr98o0mqchrkz.png" alt="The later prototype showing a populated transaction ledger, selected transaction editor and the visible upper portion of running totals." width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The later prototype after feedback and correction. The window size and fictional examples differ from the earlier capture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There were several rounds of this. At smaller window sizes, the transaction list could paint over the right-hand panel. Contained horizontal scrolling and truncation within columns addressed that, but I later caught overlapping balance labels and overflowing rule controls as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdimfcsu7ehn59bzf637d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdimfcsu7ehn59bzf637d.png" alt="A compact balance panel with labels colliding with the input fields." width="305" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One of the compact-layout problems I caught while trying the prototype.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A successful build and a screenshot capture could coexist with something I could immediately see was awkward. For the repeatable problems, the agent added compact-window captures and geometry assertions for the ledger, inspector, balance panel, and rule controls. Those gave later changes something concrete to fail against. Deciding whether the result was clear and comfortable to use still needed me to use it.&lt;/p&gt;

&lt;p&gt;Review also found behavioural gaps. Unresolved items weren't always blocking acknowledgement, changed transactions could be silently skipped on re-import, and foreign-currency rows didn't require an actual GBP amount. Those went into a separate repair issue before the production decision.&lt;/p&gt;

&lt;p&gt;Accepting the interface direction hadn't meant accepting every behaviour in the prototype. Keeping those decisions separate let me retain the layout and address the omissions.&lt;/p&gt;

&lt;p&gt;These corrections were also giving me clearer UI requirements and checks to carry into production development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corrections needed to survive the next session
&lt;/h2&gt;

&lt;p&gt;Some other feedback also changed the rules for subsequent work.&lt;/p&gt;

&lt;p&gt;During the early hosted builds, I caught a GitHub Actions runtime deprecation warning. The Node version selected for the project was separate from the runtime used internally by an action. Updating the project's version hadn't addressed the warning.&lt;/p&gt;

&lt;p&gt;That led to an explicit repository requirement for supported packages, actions, and images. The storage work also moved from the original SQLCipher bundle, recorded as deprecated during the project, to SQLite3MC. An authenticated snapshot experiment was feasible, but would have introduced a bespoke format and whole-file writes where an embedded database already suited the work.&lt;/p&gt;

&lt;p&gt;Live totals went into the specification. Compact-layout defects became regression checks. Supported tooling became a repository rule. These were decisions a fresh session could find without me repeating the conversation.&lt;/p&gt;

&lt;p&gt;But the budget rule from part 1 hadn't been demonstrated in the early persistence work. Correction counts were recorded after the fact, and CI repairs continued beyond the recorded count without a clear budget-renewal checkpoint. Writing down three attempts or 90 minutes hadn't made the limit enforce itself. The controller still needed its own experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retaining evidence filled the storage allowance
&lt;/h2&gt;

&lt;p&gt;GitHub Actions artifact storage was a real pain, and a time sink I hadn't wanted from this experiment. It also showed how AI can still make some baffling decisions, like uploading hundreds of megabytes on every run without checking whether any of it needed keeping.&lt;/p&gt;

&lt;p&gt;The workflow uploaded application output and validation evidence from each platform. When I asked what it was keeping, the investigation estimated roughly 680 MB of compressed output across three large uploads per run. My allowance was 500 MB. Most of the output was native binaries and debugging symbols, rather than the much smaller screenshots and diagrams.&lt;/p&gt;

&lt;p&gt;Uploads were failing because the quota had been reached. Application checks could pass, then the workflow would fail during upload and block the dependent cross-platform backup cycle. I had to distinguish the checks that had passed from the validation that couldn't run.&lt;/p&gt;

&lt;p&gt;The first reduction removed the large bundles and rebuilt from the tested commit in each job, keeping only the small encrypted backups needed between platforms. I then made the policy explicit. Actions artifact storage was for release-ready application binaries and, where useful, their debugging symbols. Test data, traces, screenshots, and intermediate output didn't belong there.&lt;/p&gt;

&lt;p&gt;The final prototype workflow uploaded no artifacts. It passed the small synthetic encrypted backup through job outputs so the cross-platform restore exercise could continue. The resulting hosted run passed all seven jobs and reported zero artifacts. That transfer was specific to the small fixture, not a general mechanism for moving application databases.&lt;/p&gt;

&lt;p&gt;This changed the earlier plan to retain generated architecture output as an artifact too. LikeC4 still ran during validation; generating its output didn't require uploading it on every run.&lt;/p&gt;

&lt;p&gt;Asking the agent to retain evidence had produced a workflow whose storage needs hadn't been checked against the available allowance. Reviewing the pipeline needed to include what it kept and why. Otherwise the process intended to validate the application could prevent that validation from finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proposed permissions boundary had another route around it
&lt;/h2&gt;

&lt;p&gt;For a live tracker-recovery trial, the agent proposed narrowly scoped credentials. I questioned whether that would provide the intended restriction. I'd already needed broader permissions for legitimate project work, and the existing authenticated GitHub CLI session was still available.&lt;/p&gt;

&lt;p&gt;GitHub would enforce the permissions of the credential used for a request. Giving the controller a narrow credential wouldn't isolate an agent that could also use the broader session.&lt;/p&gt;

&lt;p&gt;I kept the trial focused on whether the controller could recover after a GitHub update. The manual tracker adapter was fixed to one sandbox issue, one project, one marked comment, and two status transitions. It exercised losing the response after GitHub had accepted a write, then recovering without duplicating the comment or repeating an already completed mutation. The broader operator permissions were recorded explicitly.&lt;/p&gt;

&lt;p&gt;That let me investigate recovery under supervision. It didn't establish a security boundary for unattended execution, which remained unapproved. That would need an isolated environment with only the intended credentials available.&lt;/p&gt;

&lt;p&gt;The controller's process-recovery tests had a separate problem on Windows. A supervisor intended to survive the controller's termination wasn't producing its recovery receipt. This time the run reached its correction limit and stopped. I explicitly authorised another attempt. Detaching the supervisor resolved the tested failure, and the hosted checks subsequently passed across all three operating systems.&lt;/p&gt;

&lt;p&gt;That was a concrete improvement over the early budget bookkeeping. The run stopped with an unresolved problem, and continuing required a decision from me.&lt;/p&gt;

&lt;p&gt;This is definitely one I want to return to, as ideally agents should only be running with the exact access they need. For this supervised trial on a personal project, I deemed the risk acceptable with the controls I'd put in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd accepted
&lt;/h2&gt;

&lt;p&gt;After working through the prototypes, I was happy to move forward with Avalonia and .NET, SQLite3MC, the guided reconciliation approach, and the small controller I'd been testing under supervision. The workflow checks had passed on macOS, Windows, and Linux, and a backup had been restored through all three platforms.&lt;/p&gt;

&lt;p&gt;The disposable shell still needed production boundaries, importer contracts, accessibility acceptance, security decisions, and packaging work. Passing the prototypes hadn't turned it into a finished financial application.&lt;/p&gt;

&lt;p&gt;I also asked for an isolated trial with my real data and actual flow once the complete workflow was available, before release packaging. Synthetic fixtures meant development and validation could happen without exposing financial records. The later trial would check whether the application fitted the job I needed it for, with me operating it privately and keeping the records out of the agents, repository, and CI. I still wanted to try it with my own data before considering it ready for release.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>learning</category>
    </item>
    <item>
      <title>Building an agentic workflow, part 1: deciding what done means</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:22:22 +0000</pubDate>
      <link>https://dev.to/nullabletype/building-an-agentic-workflow-part-1-deciding-what-done-means-35dm</link>
      <guid>https://dev.to/nullabletype/building-an-agentic-workflow-part-1-deciding-what-done-means-35dm</guid>
      <description>&lt;p&gt;I still reconcile my credit-card spending manually across several spreadsheets. The calculations and automation I've added help, but it's still painful to do every month.&lt;/p&gt;

&lt;p&gt;That's the reason for &lt;a href="https://github.com/nullabletype/dot-finance" rel="noopener noreferrer"&gt;dot-finance&lt;/a&gt;. I want an application that imports transactions, works out how much needs to come from different accounts, and records what's been settled. It will stay offline and I'll still make the transfers myself.&lt;/p&gt;

&lt;p&gt;There's another part to the project though. I've used coding agents on plenty of bounded tasks, and full agentic workflows in my job as a software architect. I want to build my own development loop from scratch for personal projects, with dot-finance as a real application to test it against.&lt;/p&gt;

&lt;p&gt;That means working out what happens around the coding. An agent needs enough context to pick up an issue in a fresh session, a way to check its work, and a limit on how long it can spend trying to correct a failure. If a run stops halfway through, I need to know where it got to without reconstructing the whole conversation.&lt;/p&gt;

&lt;p&gt;Before writing application code, I started by putting those rules into the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context that survives the conversation
&lt;/h2&gt;

&lt;p&gt;I began with a specification and asked Codex to review it and grill me on anything unclear before planning the work.&lt;/p&gt;

&lt;p&gt;I've been using reusable agent skills for this, including ideas from &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skills collection&lt;/a&gt;. A skill gives the agent instructions for a particular kind of work, which saves rebuilding the same prompt each time. The local &lt;code&gt;grilling&lt;/code&gt; skill asks one question at a time, gives a recommendation, and waits for a decision. I then used &lt;code&gt;to-issues&lt;/code&gt; to turn the prototype work into &lt;a href="https://github.com/nullabletype/dot-finance/issues" rel="noopener noreferrer"&gt;GitHub tickets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The conversation is useful for reaching decisions, but a later session needs to find the decisions without reading everything that led to them. I've split that context across three files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/nullabletype/dot-finance/blob/main/spec-file.md" rel="noopener noreferrer"&gt;&lt;code&gt;spec-file.md&lt;/code&gt;&lt;/a&gt; describes what the application must do.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nullabletype/dot-finance/blob/main/development-workflow.md" rel="noopener noreferrer"&gt;&lt;code&gt;development-workflow.md&lt;/code&gt;&lt;/a&gt; describes how an issue moves through implementation, validation, review, and integration.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nullabletype/dot-finance/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; gives an agent the operating rules for the repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, staying offline is an application constraint. Using synthetic development data is a rule for doing the work. A missing platform check is unfinished validation. All three need to survive the session in which they were discussed, and each belongs somewhere an agent can find it again.&lt;/p&gt;

&lt;p&gt;This also gives me something concrete to review when the process needs changing. I can update the relevant rule rather than leave a correction buried in a chat and hope the next run sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One issue at a time
&lt;/h2&gt;

&lt;p&gt;The initial loop is sequential. It selects one ready issue with completed dependencies, works on an isolated branch, runs the required checks, and sends the result through independent review before integration.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The proposed loop at this stage of the project. The workflow is documented; the controller hasn't been built.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Starting with one issue keeps the coordination manageable. There's one branch to inspect and one set of expected outcomes. I can work out whether the basic loop behaves properly before adding concurrent work and the extra problems that brings.&lt;/p&gt;

&lt;p&gt;The issue needs to say how I'll know the work is complete. “Add encrypted backup” leaves too much open. Saving synthetic data on one supported platform, restoring it on another, and checking that the expected state survived gives the agent a result to demonstrate.&lt;/p&gt;

&lt;p&gt;Opening a pull request doesn't satisfy that requirement. A green build might not satisfy it either, if the required behaviour was never exercised. The issue stays open until the agreed validation and integration are complete.&lt;/p&gt;

&lt;p&gt;The evidence also needs to identify the commit it came from. If a review leads to code changes, the affected checks have to run again. A Windows check that passed before those changes can't simply be carried forward because it's on the same branch.&lt;/p&gt;

&lt;p&gt;It's a small bookkeeping requirement with a practical consequence. After an interruption, I need to be able to tell which code passed which checks, and which checks are still pending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting a limit on corrections
&lt;/h2&gt;

&lt;p&gt;In my earlier AI field notes, I described using Gemini to help set up an Octopus Deploy instance. I kept feeding errors back into the conversation, and it eventually suggested running Octopus on PostgreSQL. By then I should have stepped back, checked what was missing from the context, and started again from a better position.&lt;/p&gt;

&lt;p&gt;I don't want this loop to depend on me noticing that point every time.&lt;/p&gt;

&lt;p&gt;For the prototype phase, I've set an initial limit of three correction attempts or 90 minutes of active agent work, whichever comes first. Those numbers are provisional. They give the first runs a stopping point while I find out whether the limits are useful in practice.&lt;/p&gt;

&lt;p&gt;When the budget runs out, the controller should return enough information to investigate or continue deliberately. That includes the issue, branch and commit, checks completed, evidence collected, correction count, remaining problem, and next action. Continuing needs an explicit new budget.&lt;/p&gt;

&lt;p&gt;An interruption needs the same care. Restarting a run mustn't reset its correction count or turn an unfinished check into a pass. The plan is to keep GitHub as the record of the work, with a local checkpoint holding enough execution state to resume accurately.&lt;/p&gt;

&lt;p&gt;The controller also needs to distinguish a code failure from an infrastructure problem. If a hosted runner is unavailable, editing the application won't help. That platform's validation should remain pending, with the blocker recorded. Otherwise the loop could spend its correction budget changing code that hasn't been shown to be wrong.&lt;/p&gt;

&lt;p&gt;These are behaviours I need to test in the &lt;a href="https://github.com/nullabletype/dot-finance/tree/main/prototypes/ControllerTrial" rel="noopener noreferrer"&gt;controller itself&lt;/a&gt;. The prototype plan includes a deliberately failing check, an interrupted run, an unavailable runner, and a review rejection. A successful application build wouldn't tell me whether any of those paths work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing the tests and the architecture
&lt;/h2&gt;

&lt;p&gt;Independent review needs to look beyond whether the implementation compiles. Code and tests written together can share the same misunderstanding, so the reviewer needs the requirement, the diff, and the validation evidence. Part of that review is deciding whether the tests would catch a meaningful error in the required behaviour.&lt;/p&gt;

&lt;p&gt;I've also asked for &lt;a href="https://github.com/nullabletype/dot-finance/tree/main/docs/architecture" rel="noopener noreferrer"&gt;LikeC4 architecture diagrams&lt;/a&gt; alongside the implementation. I've been a long-term fan of the C4 model and the consistency it brings to architectural discussions. This project gives me a chance to try architecture as code while an agent is making the implementation changes.&lt;/p&gt;

&lt;p&gt;The proposed rule is to update the LikeC4 model in the same pull request when component responsibilities, dependencies, storage, or significant flows change. If a change has no architectural effect, the pull request can say so.&lt;/p&gt;

&lt;p&gt;The build is intended to validate the model and generate the diagrams. That would catch an invalid model, but it wouldn't establish that the diagram accurately describes the C# code. Checking that agreement remains part of review. Otherwise I could end up with a passing build and an increasingly misleading description of the application.&lt;/p&gt;

&lt;p&gt;At the end of this setup stage, I had a specification, prototype tickets, and workflow rules. There was no application or controller yet. The first prototype was planned to establish the desktop and LikeC4 build foundation, followed by investigations into encrypted storage, restoration across platforms, and the guided reconciliation flow. Alongside those application checks, the controller still had to demonstrate that it could stop within its budget and resume without losing track of unfinished validation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>development</category>
    </item>
    <item>
      <title>AI Field Notes 2026</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Fri, 03 Jul 2026 22:19:36 +0000</pubDate>
      <link>https://dev.to/nullabletype/ai-field-notes-2026-53ap</link>
      <guid>https://dev.to/nullabletype/ai-field-notes-2026-53ap</guid>
      <description>&lt;p&gt;There is no doubt that AI has rocked the world of coding considerably over the last couple of years. One thing that has become apparent, through keeping up with the whirlwind of changes, is that people are still figuring out how to use these tools effectively. It seems like every video I watch, or article I read, people are swearing they've found the killer approach.&lt;/p&gt;

&lt;p&gt;I have noticed through all the noise though, that there are some key concepts that seem common regardless of which approach you take. This isn't comprehensive, and is likely to change over time, but my key observations in July 2026 are:&lt;/p&gt;

&lt;h2&gt;
  
  
  Clarity is king
&lt;/h2&gt;

&lt;p&gt;I recently watched this excellent &lt;a href="https://www.youtube.com/watch?v=yhGzXULZkEw" rel="noopener noreferrer"&gt;TED Talk by Rainer Stropek&lt;/a&gt;. His closing remark is "I am not just a coder, I am a developer and my new programming language is clarity". The talk is well worth a watch for various reasons, but this statement definitely struck a chord with me.&lt;/p&gt;

&lt;p&gt;Clarity, when working with AI agents, is key. This isn't a new thing though, as if you were working with a team on delivering a coding project, having a shared context and understanding of the goal and approach is key. That doesn't change when working with AI. Having that shared understanding of not just the task, but the goal and the process leading there will give you much better results than just stating a task and expecting great results.&lt;/p&gt;

&lt;p&gt;Rather than just state the task, state the goal you want to achieve and the key criteria or guardrails to get there. Use the LLM to help plan and build your spec before letting it loose. A lot of harnesses have planning modes, so give them a try. Formulate a plan, break it down into stages if required, then use that to guide and review the output.&lt;/p&gt;

&lt;p&gt;"&lt;a href="https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/SKILL.md" rel="noopener noreferrer"&gt;Grill me relentlessly&lt;/a&gt;", as Matt Pocock recommends, to work towards that shared understanding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Weak:
Add task cancellation to the CLI.

Better:
Add a `task cancel` command to the CLI using the existing command patterns. 

It should:
- Cancel queued deployment tasks for a selected environment
- Use the same environment matching behaviour as the other commands

Ensure you:
- Include unit tests and live integration tests
- Update any user-facing help text if the command surface changes
- Run the unit test suite, including integration tests
- Provide a summary of your changes and validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Context is also king
&lt;/h2&gt;

&lt;p&gt;A follow-up point from the above: be surgical about the context you provide. Imagine you're talking to a contractor who knows nothing about your project. Be clear on the technical stack and constraints. Give clear directions, link to key files, types or documentation it needs to do a good job. Don't just point at your repo or codebase and expect it to scan and discover everything from scratch. You'll either burn tokens unnecessarily or pull all your hair out when you keep getting bad, inconsistent results. Even worse, both. High signal, low noise.&lt;/p&gt;

&lt;p&gt;Prefer smaller, reviewable tasks. Give clear boundaries and observable success criteria where possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Weak:
Look through the repo and work out how commands are built.

Better:
The existing command patterns are in `src/ShipItSharp.Console/Commands`. 

Start with `BaseCommand`, `Environment`, and `Channel` as examples. 

Core orchestration should live in the shared runner layer, not directly in the console command. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  You're not just stuck in the terminal
&lt;/h2&gt;

&lt;p&gt;I picked this up from &lt;a href="https://www.youtube.com/watch?v=fdbXNWkpPMY" rel="noopener noreferrer"&gt;a talk by Lucas Meijer&lt;/a&gt;, who offered a simple tip that made my comprehension and efficiency so much better. Ask for a single-page HTML summary. That's it, but it's amazingly powerful in the right situation.&lt;/p&gt;

&lt;p&gt;One example use case is I asked Codex to "grill me" on a complex goal and it generated 60 questions for me to review. Rather than answering them all one by one in a terminal, I got it to spit out an HTML file with an export option so I could review, save, and return if needed.&lt;/p&gt;

&lt;p&gt;I've also used it to generate reviews of code, produce a detailed plan of action or even show me design choices to choose between when I've been unsure.&lt;/p&gt;

&lt;p&gt;Obviously, don't overuse this as it can burn tokens, but in the right contexts it can produce a much easier response to comprehend and lead you to a more efficient goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chat context is expensive
&lt;/h2&gt;

&lt;p&gt;It's a general recommendation that you keep to a low number of messages per chat or session. Why? Every time you send a message, the entire previous context is also sent. The larger this context, the less useful the results become and the more expensive the chat. Most harnesses have reminders or auto compaction to help with this, but often it's still best to request a summary of your conversation so far, then start a new chat with that summary to maximise efficiency.&lt;/p&gt;

&lt;p&gt;On a similar note, if your instructions were unclear and you got a duff response back, don't scorn the AI tool and tell it to try again. Consider editing your original message so it's clearer and then resend. It keeps your context shorter, will give you a more refined response and will burn fewer tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get AI to validate its own work
&lt;/h2&gt;

&lt;p&gt;Before you set your agent off on a series of tasks, make sure you define sensible validation for the result. Think about what you would expect if you were doing the work yourself. Unit tests that pass covering specific areas? Second pass review? Integration tests? Browser validation? Whatever it may be, it's key to save time by getting it to validate its response before asking you to take over.&lt;/p&gt;

&lt;p&gt;One tip I picked up from a colleague is to task the agent with continuing to refine until it's 95% confident on an answer. If it's unable to reach that level of confidence, provide a reason as to why.&lt;/p&gt;

&lt;p&gt;A pattern I’ve started using is asking Codex to do a second-pass review against the original goal, not just against compiler errors. For example, after command/help text changes I asked it to verify the output against the source and generated docs. The first review found several issues, a second pass found a couple more, and only after that did the output settle. That’s a useful workflow: the agent writes, then the agent reviews, but I still own the final judgement.&lt;/p&gt;

&lt;p&gt;Another example is I recently used Codex to add new task-management commands to &lt;a href="https://shipitsharp.app" rel="noopener noreferrer"&gt;ShipItSharp&lt;/a&gt;. The useful part wasn’t that it generated the code. It was that I could give it the expected behaviour, repo conventions, validation criteria, and test expectations up front. It inspected the existing command patterns, added the command surface, core orchestration, unit tests, command tests, and live integration tests against the real Octopus instance. The first pass missed part of my integration-test expectation, which was a good reminder that the agent still needs senior review and clear acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-strike rule
&lt;/h2&gt;

&lt;p&gt;When an agent's code throws an error, give it one shot to fix it. If it fails a second time, step away from the prompt window. Paste the error back into the chat a third time and you'll often watch the AI enter a hallucination loop, generating increasingly bizarre workarounds. Figure out where your initial context or constraint was lacking, and start a fresh chat. Know when to cut your losses.&lt;/p&gt;

&lt;p&gt;A perfect example of this is when I was working on &lt;a href="https://shipitsharp.app" rel="noopener noreferrer"&gt;ShipItSharp&lt;/a&gt;. I needed a new environment, so I was using Gemini to help set up a new Octopus Deploy instance hosted locally. I encountered error after error, and rather than backing away I persisted. In the end, it hallucinated that Octopus Deploy can run on PostgreSQL. After calling it out, its response was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I owe you a massive, deeply embarrassing apology.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lesson learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think of using AI agents as a "layer up"
&lt;/h2&gt;

&lt;p&gt;This calls back to &lt;a href="https://www.youtube.com/watch?v=yhGzXULZkEw" rel="noopener noreferrer"&gt;Rainer Stropek's talk&lt;/a&gt;, where he talks about the shift from being on the ground writing code to moving another layer up to orchestrate the tool to do the boilerplate stuff. Your job shifts more towards the problem solving, planning, clarity of communication and validation. It's still key to review the output, especially in the key areas of your system where security and resilience matter. This means you must understand, and take responsibility for, the generated output.&lt;/p&gt;

&lt;p&gt;A good example for me was release workflow work. I didn't need Codex just to edit YAML. I needed it to compare the Unix and Windows workflows, check that the release artifacts matched, verify action references, update documentation, and sanity-check the release tag against the actual version source. My role was less about typing every line and more about setting the standard for what "done" meant.&lt;/p&gt;

&lt;p&gt;It honestly reminds me of earlier in my career. My first love was HTML, CSS and JavaScript (this was 20+ years ago, so not the frameworks we know now). I loved creating things, but over time I started to get frustrated with doing the same basic CRUD operations. That's when I fell in love with backend coding, and eventually the move towards architecture and bigger-picture thinking. The key through all of this is the love of problem solving. Seeing an issue, finding a solution, and building a solution.&lt;/p&gt;

&lt;p&gt;AI tools are like that next level up, where the key things that make you a great developer are still absolutely relevant. Anyone can vibe code something, but building something resilient, secure, safe and scalable for thousands if not millions of users still requires strong development skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI is not human, don't treat it like it is
&lt;/h2&gt;

&lt;p&gt;It may sound harsh, but its true. It's only natural to want to personify the AI tools we use. After all, it often responds to you as if it was a friend or colleague. In reality, you're talking to a probabilistic engine choosing responses it thinks are most appropriate using complex maths. It has no feelings or empathy. Keep operational prompts concise.&lt;/p&gt;

&lt;p&gt;Most harnesses these days have an option to change its personality. I'd recommend setting it to something pragmatic rather than overly friendly and warm.&lt;/p&gt;

&lt;p&gt;Do you have any tips you've found useful? Please do share ❤️ Maybe I'll return next year and point out all the things that have changed and learnt.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>fieldnotes</category>
    </item>
    <item>
      <title>Proxy any class by interface in C# with DispatchProxy</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Fri, 22 Oct 2021 22:27:23 +0000</pubDate>
      <link>https://dev.to/nullabletype/proxy-any-class-by-interface-in-c-with-dispatchproxy-2i6a</link>
      <guid>https://dev.to/nullabletype/proxy-any-class-by-interface-in-c-with-dispatchproxy-2i6a</guid>
      <description>&lt;p&gt;I was recently tasked with figuring out a way to performance test database calls being made via &lt;code&gt;NHibernate&lt;/code&gt; to &lt;code&gt;PostgreSQL&lt;/code&gt;. As far as I could find, &lt;code&gt;NHibernate&lt;/code&gt; doesn't expose any really easy ways to intercept and manually log both the &lt;code&gt;SQL&lt;/code&gt; and execution times for each query. &lt;/p&gt;

&lt;p&gt;After a little more digging, I came across a handy lesser-known tool that allows you to easily proxy any interface, be it user-defined or from a third party library. This tool lives in the &lt;a href="https://www.nuget.org/packages?q=System.Reflection.DispatchProxy" rel="noopener noreferrer"&gt;&lt;code&gt;System.Reflection.DispatchProxy&lt;/code&gt;&lt;/a&gt; nuget package. With a little code, you can wrap any instance of a class that implements an interface with a proxy to not only log but also manipulate both arguments and returned data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Take this simple class and interface.&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IHello&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;SayHello&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;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hello&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHello&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;SayHello&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Hello &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&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;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing special, only one method exposed which takes a simple name parameter, writes out a string to the console and returns a bool indicating success.&lt;/p&gt;

&lt;p&gt;Say, although contrived in this example, we wanted to record calls going to &lt;code&gt;SayHello&lt;/code&gt;.&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;class&lt;/span&gt; &lt;span class="nc"&gt;HelloDispatchProxy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DispatchProxy&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IHello&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IHello&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt; &lt;span class="n"&gt;targetMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Code here to track time, log call etc.&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="n"&gt;targetMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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;result&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;static&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;CreateProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;target&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;proxy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HelloDispatchProxy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;HelloDispatchProxy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
        &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&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;proxy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is our &lt;code&gt;DispatchProxy&lt;/code&gt;, &lt;code&gt;HelloDispatchProxy&lt;/code&gt;. Usage is pretty simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;IHello&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HelloDispatchProxy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHello&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateProxy&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;Hello&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CreateProxy&lt;/code&gt; method calls &lt;code&gt;Create&amp;lt;T&amp;gt;&lt;/code&gt; on the base abstract class to generate a proxy object for you. The target instance is then assigned to the &lt;code&gt;Target&lt;/code&gt; property before returning the proxy cast to type &lt;code&gt;T&lt;/code&gt; (&lt;code&gt;IHello&lt;/code&gt; in this instance).&lt;/p&gt;

&lt;p&gt;Any method that is then called on the proxied &lt;code&gt;IHello&lt;/code&gt; instance will trigger &lt;code&gt;Invoke()&lt;/code&gt;. It's your proxies responsibility to call the target method, pass through the parameters and return the result. In this method, you can do whatever you like, and you'll also have access to your original &lt;code&gt;Target&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;You can see a complete example of this over on my &lt;a href="https://github.com/nullabletype/scratchpad/blob/main/DispatchProxyExample/DispatchProxyExample/HelloDispatchProxy.cs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With great power comes great responsibility&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This allows you quite a lot of control, so be careful what you do with &lt;code&gt;args&lt;/code&gt; and the returned result or you can cause some unexpected behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance testing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DispatchProxy&lt;/code&gt; lives under the &lt;code&gt;System.Reflection&lt;/code&gt; namespace. Because of this, I was curious about what overhead is introduced by this approach. What better way to understand than to test it myself.&lt;/p&gt;

&lt;p&gt;I set up a &lt;a href="https://github.com/nullabletype/scratchpad/blob/main/DispatchProxyExample/DispatchProxyExample/Program.cs" rel="noopener noreferrer"&gt;slightly over-engineered set of tests&lt;/a&gt; to give a raw comparison between calls to both an unproxied and proxied instance. I ran these multiple times to get an average before analysing the results. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    100000 W/O  100000 W    % Increase
    2233.023    2382.8835   6.71%
    2235.5306   2398.1216   7.27%
    2236.2449   2386.4416   6.72%
    2236.8529   2389.209    6.81%
    2239.9692   2402.4627   7.25%
    2227.0108   2367.0306   6.29%
    2237.8432   2384.8429   6.57%
    2233.5528   2392.8394   7.13%
    2238.411    2398.3124   7.14%
    2235.7398   2388.134    6.82%
    2262.3454   2380.0595   5.20%
    2340.0243   2385.0553   1.92%
    2249.3693   2383.6925   5.97%
    2246.9835   2381.507    5.99%
    2240.7711   2381.4675   6.28%
    2232.5296   2374.0465   6.34%
    2248.0095   2359.8636   4.98%
    2250.2518   2394.8422   6.43%
    2234.7757   2387.8114   6.85%
    2238.7645   2387.8047   6.66%


    0.00112245  0.001192661 6.27%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So on average, the execution took an extra 6 or so percent with just the introduction of the proxy. Bear in mind in this current state, the proxy is doing nothing useful at all, but this helps put into context the efficiency.&lt;/p&gt;

&lt;p&gt;But if you were to add something slow like a blocking console write line in the proxy, the % increase shoots up from 6.27% to over 100%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Going to call &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;targetMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important part with the performance impact is what you decide to do in the &lt;code&gt;Invoke&lt;/code&gt; method, something you'll need to benchmark yourself.&lt;/p&gt;

&lt;p&gt;In all but the highest performance-critical applications, the overhead of the proxy itself is minor. If you take for example proxying 2-3 DB calls in the context of something like a HTTP web request or message handler the impact is negligible. In fact, in my testing with a real-world application, the numbers are within an acceptable margin of error even when tracing into an &lt;a href="https://en.wikipedia.org/wiki/Application_performance_management" rel="noopener noreferrer"&gt;APM&lt;/a&gt; tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  NHibernate &amp;amp; PostgreSQL
&lt;/h2&gt;

&lt;p&gt;If you're specifically interested in collecting information from &lt;code&gt;NHibernate&lt;/code&gt; as I was, you can do this by wrapping the &lt;code&gt;IDBCommand&lt;/code&gt; in a custom driver implementation.&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;class&lt;/span&gt; &lt;span class="nc"&gt;ProfiledNpgsqlDriver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NpgsqlDriver&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;override&lt;/span&gt; &lt;span class="n"&gt;IDbCommand&lt;/span&gt; &lt;span class="nf"&gt;CreateCommand&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;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateCommand&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;ConfigurationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppSettings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"EnableDBTracing"&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="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProfiledDbCommandDispatchProxy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDbCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You can access the command being executed from the target &lt;code&gt;IDBCommand&lt;/code&gt;'s &lt;code&gt;Command&lt;/code&gt; property and record time etc. with a &lt;code&gt;StopWatch&lt;/code&gt;. I went a step further and implemented the proxy as an &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=net-5.0" rel="noopener noreferrer"&gt;&lt;code&gt;IObservable&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt; so I can handle it differently depending on the use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Hopefully, you found this interesting. Can you think of any good use cases for this, or have any comments? Would love to hear them ❤&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>proxy</category>
      <category>dotnet</category>
      <category>programming</category>
    </item>
    <item>
      <title>Auto OpenVPN reconnect and killswitch for VPNs with dynamic IPs</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Sat, 02 Oct 2021 22:22:32 +0000</pubDate>
      <link>https://dev.to/nullabletype/auto-openvpn-reconnect-and-killswitch-for-vpns-with-dynamic-ips-4fnm</link>
      <guid>https://dev.to/nullabletype/auto-openvpn-reconnect-and-killswitch-for-vpns-with-dynamic-ips-4fnm</guid>
      <description>&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;If you’re like me and run a home Linux server, you may wish to add some extra security in the form of a VPN connection to ensure traffic is encrypted. In my case, I use a Raspberry Pi to backup things like RAW images from my camera to an off-site location. The extra security gives me a little peace of mind, especially as the server generally uses a cellular connection.&lt;/p&gt;

&lt;p&gt;I’ve been using &lt;code&gt;UFW&lt;/code&gt; to work as a killswitch and only allow traffic via the VPN connection, but one issue I've had is with my trusted VPN provider is that they regularly rotate IP addresses. &lt;code&gt;UFW&lt;/code&gt; uses &lt;code&gt;iptables&lt;/code&gt; under the hood, meaning it can only be configured to use IP addresses explicitly and not a domain name. This has left my backup machine without an internet connection fairly frequently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Just to set this straight from the start, I’m no UNIX genius. I spend most of my time working with Windows machines and use Linux more as a hobbyist. That been said, I’ve written my fair share of code/scripts.&lt;/p&gt;

&lt;p&gt;My solution is pretty simple; when disconnects happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; fetch a fresh list of IP’s from my internal DNS server.&lt;/li&gt;
&lt;li&gt; Update &lt;code&gt;UFW&lt;/code&gt;&amp;nbsp;.&lt;/li&gt;
&lt;li&gt; Update the &lt;code&gt;OpenVPN&lt;/code&gt;connection file and reconnect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I thought I’d share my approach, as I failed finding any other solution online so this may be of help to others.&lt;/p&gt;

&lt;p&gt;The first part is fetching the list of domains. To do this, I use &lt;a href="https://downloads.isc.org/isc/bind9/cur/9.17/doc/arm/html/manpages.html#dig-dns-lookup-utility" rel="noopener noreferrer"&gt;dig&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ipoutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig &lt;span class="nv"&gt;$1&lt;/span&gt; +short&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; “&lt;span class="nv"&gt;$ipoutput&lt;/span&gt;” &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “No ips found &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;”  
  &lt;span class="nb"&gt;exit &lt;/span&gt;1  
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “Found ips &lt;span class="nv"&gt;$ipoutput&lt;/span&gt;”

readarray &lt;span class="nt"&gt;-t&lt;/span&gt; ips &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;”&lt;span class="nv"&gt;$ipoutput&lt;/span&gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me a nice list of IP address for the specified domain.&lt;/p&gt;

&lt;p&gt;Now I know the correct ip addresses, I need to make use of them. The first job is to update &lt;code&gt;UFW&lt;/code&gt;. I decided to use a template file so I can set it up as I want for each machine. This may not be the most elegant solution, but its pretty flexible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ufwoutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;’’

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; “&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;”  
&lt;span class="k"&gt;do&lt;/span&gt;  
  :  
  ufwoutput+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$”&lt;/span&gt;&lt;span class="c"&gt;### tuple ### allow udp any $i any 0.0.0.0\\/0 out\\n”  &lt;/span&gt;
  ufwoutput+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$”&lt;/span&gt;&lt;span class="nt"&gt;-A&lt;/span&gt; ufw-user-output &lt;span class="nt"&gt;-p&lt;/span&gt; udp &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT&lt;span class="se"&gt;\\&lt;/span&gt;n&lt;span class="se"&gt;\\&lt;/span&gt;n”  
&lt;span class="k"&gt;done

if &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp”&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “Deleting tmp file…”  
  &lt;span class="nb"&gt;rm&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp”  
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.template” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp”

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; “s/&lt;span class="se"&gt;\[&lt;/span&gt;content&lt;span class="se"&gt;\]&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ufwoutput&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/g” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp”

&lt;span class="nb"&gt;mv&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/user.rules.tmp” /etc/ufw/user.rules

&lt;span class="nb"&gt;echo&lt;/span&gt; “Reloading ufw”  
ufw reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script builds up a string containing the correct formatted lines for the &lt;code&gt;user.rules&lt;/code&gt; file for &lt;code&gt;UFW&lt;/code&gt;, creates a new copy of the template, replaces &lt;code&gt;[content]&lt;/code&gt; in the file using &lt;code&gt;sed&lt;/code&gt; then moves the file into the correct place (for ubuntu). The final thing to do is &lt;code&gt;ufw reload&lt;/code&gt; to reload the &lt;code&gt;user.rules&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;That’s &lt;code&gt;UFW&lt;/code&gt; taken care of, time for &lt;code&gt;OpenVPN&lt;/code&gt; using a similar method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;openvpnoutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;’’

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; “&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;”  
&lt;span class="k"&gt;do&lt;/span&gt;  
:  
  openvpnoutput+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$”&lt;/span&gt;remote &lt;span class="nv"&gt;$i&lt;/span&gt; 1198&lt;span class="se"&gt;\\&lt;/span&gt;n”  
&lt;span class="k"&gt;done

if &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp”&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “Deleting tmp file…”  
  &lt;span class="nb"&gt;rm&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp”  
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.template” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp”

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; “s/&lt;span class="se"&gt;\[&lt;/span&gt;content&lt;span class="se"&gt;\]&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;openvpnoutput&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/g” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp” “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp”

&lt;span class="nb"&gt;mv&lt;/span&gt; “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn.tmp” “openvpn.ovpn”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in a similar way to the &lt;code&gt;UFW&lt;/code&gt; code, replacing a &lt;code&gt;[content]&lt;/code&gt; placeholder in the&amp;nbsp;&lt;code&gt;.opvn&lt;/code&gt; template file.&lt;/p&gt;

&lt;p&gt;So that’s all the code to update &lt;code&gt;UFW&lt;/code&gt; and &lt;code&gt;OpenVPN&lt;/code&gt;. The next thing to do is to manage the connection. I do this via a simple script called from a &lt;code&gt;cron&lt;/code&gt; job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;getStatus &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; “Attempting to get device status…”  
  ip address show | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1  
  &lt;span class="k"&gt;return &lt;/span&gt;0  
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a pretty straightforward function. &lt;code&gt;grep&lt;/code&gt; the output from &lt;code&gt;ip address show&lt;/code&gt; to see if the &lt;code&gt;OpenVPN&lt;/code&gt; connection is live. Return either &lt;code&gt;1&lt;/code&gt;for success or &lt;code&gt;0&lt;/code&gt;for failure.&lt;/p&gt;

&lt;p&gt;The last thing to do is to check the result and connect if required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getStatus &lt;span class="nv"&gt;$device&lt;/span&gt;  
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “openvpn is not connected!”  
  &lt;span class="nb"&gt;echo&lt;/span&gt; “Reconnecting!”

  &lt;span class="c"&gt;#Update config  &lt;/span&gt;
  updateIps &lt;span class="nv"&gt;$domain&lt;/span&gt; &amp;amp;&amp;gt; &lt;span class="nv"&gt;$dir_path&lt;/span&gt;/renew.out &amp;amp;disown

  openvpn — config “&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/openvpn.ovpn” &amp;amp;&amp;gt;&lt;span class="nv"&gt;$dir_path&lt;/span&gt;/out.out &amp;amp;disown  
&lt;span class="k"&gt;else  
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; “openvpn is connected!”  
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output from &lt;code&gt;getStatus&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, then it will call to update the IP addresses and trigger a new &lt;code&gt;OpenVPN&lt;/code&gt; connection with &lt;code&gt;&amp;amp;disown&lt;/code&gt; to not wait for the connection to exit.&lt;/p&gt;

&lt;p&gt;Now that’s all in place, all that’s needed to run the script automatically is a &lt;code&gt;cron&lt;/code&gt; job to check as frequently as you wish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/1 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /home/account/openvpn &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./auto-connect.sh “my.vpn.domain” “tun0” &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; out.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this is useful to you, rather than having to copy &amp;amp; paste the code I’ve wrapped all this up into a &lt;a href="https://github.com/nullabletype/openvpntools" rel="noopener noreferrer"&gt;single script available on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrap Up
&lt;/h3&gt;

&lt;p&gt;As I say, I’m no Linux sysadmin but this script solves a problem for me and has been working well for the last few months. If anyone has any suggestions for improvements or comments I’d love to hear them. Even better, &lt;a href="https://github.com/nullabletype/openvpntools" rel="noopener noreferrer"&gt;fork the repo on GitHub&lt;/a&gt; and pop in a merge request ❤&lt;/p&gt;

</description>
      <category>openvpn</category>
      <category>vpn</category>
      <category>security</category>
      <category>linux</category>
    </item>
    <item>
      <title>Thoughts on fine tuning your tech CV</title>
      <dc:creator>Steven Davies</dc:creator>
      <pubDate>Sat, 02 Oct 2021 21:53:16 +0000</pubDate>
      <link>https://dev.to/nullabletype/thoughts-on-fine-tuning-your-tech-cv-1f3l</link>
      <guid>https://dev.to/nullabletype/thoughts-on-fine-tuning-your-tech-cv-1f3l</guid>
      <description>&lt;p&gt;It's almost cruel that your first step into a potential new role often falls to a few A4 sides attempting to summarise yourself; your skills, your drive, your goals. You're more than that can cover. The person reviewing your CV knows that, but they face a difficult decision on whether to invest the time to get to know you based on this first introduction.&lt;/p&gt;

&lt;p&gt;I've been responsible for hiring into multiple technical positions over the years across different skill areas including developers, testers and sys ops engineers. Some of these have been recent graduates, others looking for senior positions after decades in the industry. &lt;/p&gt;

&lt;p&gt;I've seen a wide array of CV styles, some that have delighted me and some that have been difficult to navigate. I thought it may be useful to someone out there, or maybe just myself, to list out some of the suggestions I can think of to help make your tech CV easier to digest. These are all based on real examples I've seen more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consider your audience
&lt;/h2&gt;

&lt;p&gt;This isn't just for your CV's, this is for everything. Whether you're applying for a new position, putting in a merge request to your peers or writing a best man's speech... it's important to put yourself in the shoes of the person you're writing for. What do they want to know?&lt;/p&gt;

&lt;p&gt;Regarding CV's, imagine you're in the position of hiring for the job &lt;strong&gt;you&lt;/strong&gt; want. What would you want to know, what information matters and just as importantly what information doesn't?&lt;/p&gt;

&lt;h2&gt;
  
  
  The opener
&lt;/h2&gt;

&lt;p&gt;Although it is unlikely to be the first section that is read, a personal statement is an opportunity to impress and set a clear indication about what you're looking for and have achieved. If well written, it can easily help you stand out from other candidates.&lt;/p&gt;

&lt;p&gt;One of the biggest traps I've seen people fall into is to state the basic cliches. I'm talking about things like "hard-working" or "dedicated", they're really expected traits and don't need to be said. Space on your CV is precious, and I'm sure there are things that you're proud of you want to tell the reader about over basic statements.&lt;/p&gt;

&lt;p&gt;The second thing I often see is an overuse of vocabulary. You don't need to impress with fancy words, do so by saying what you've achieved in a clear and easy to read manner.&lt;/p&gt;

&lt;p&gt;Pick out the key accomplishments and skills you want the reader to know about you, what the position you're looking for is and the direction of your career. Be honest, you don't want to lie and get an interview for a position you don't want, but feel free to tailor it to the position you're applying to. Keep the fancy words to a minimum, but don't be afraid to be confident about what you're good at. &lt;/p&gt;

&lt;p&gt;Here's an example of what I'd consider a good personal statement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An &lt;strong&gt;ISTQB certified&lt;/strong&gt; software developer in test with &lt;strong&gt;8 years' experience&lt;/strong&gt; testing enterprise level web applications. &lt;strong&gt;Designed, written and maintained frameworks&lt;/strong&gt; that ensure a &lt;strong&gt;high level of quality and resilience&lt;/strong&gt;, including testing web API's and applications for functionality and &lt;strong&gt;performance&lt;/strong&gt; that have &lt;strong&gt;reduced required testing time by 50%&lt;/strong&gt;. Thrives on the opportunity to &lt;strong&gt;mentor other testers&lt;/strong&gt;, work collaboratively in a team and has a keen eye for spotting opportunities for improvement. Now looking for the next challenge to use the skills gained to make a difference and continue professional growth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Size matters
&lt;/h2&gt;

&lt;p&gt;If your CV is jam-packed with information covering 10+ pages, then it's going to be difficult to digest and pick out the relevant information you want the reader to see.&lt;/p&gt;

&lt;p&gt;Similarly, if your CV is a page or less, you're likely omitting really useful information about yourself and your skills. &lt;/p&gt;

&lt;p&gt;The best CV's I've personally seen are generally around 1-3 pages long. Any longer than that then you may benefit from having a tidy.&lt;/p&gt;

&lt;h3&gt;
  
  
  So how do I make the best use of the space?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Keep it relevant
&lt;/h4&gt;

&lt;p&gt;The IT sector runs at a super-fast pace. Your most recent positions are always likely to be the most relevant on your career path. Take a look at your previous employment. That job you did 5+ years ago using some obscure or out of favour tech... ask yourself, do I want to even do that thing again? Is it relevant to this position or where I want my career to go? &lt;/p&gt;

&lt;p&gt;The further back your experience the less specific you probably need to be. Don't omit previous positions altogether, but you can afford more brevity the further back you go. One size doesn't fit all, just use the space wisely.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary of tech skills
&lt;/h4&gt;

&lt;p&gt;We use so many tools, so many processes and workflows... you want the person reviewing your CV to see all the wonderful things you know and have used. This can quickly turn into 1/4 of a page of your CV or a giant list of unsorted tools and skills. &lt;/p&gt;

&lt;p&gt;The problem is if you're not careful you can really hurt its usefulness. Take a look at the following example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;C#, Visual studio, MVC, WebApi, Postgresql, MongoDB, T-SQL, Flask, SQLite, LiteDB, Entity Framework, PostgreSQL, SQL Server, AWS, Python, Microsoft Word, Fiddler, Postman...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first issue is that there are quite a few things that are not relevant and just distract from what you're actually good at. Things like listing Visual studio along with C# when that's pretty much implied or listing office tools which is an expectation for any IT job. There may also be things you have no real interest in doing again, so think if it's worth listing them. Some may just be completely irrelevant for the position you're applying for.&lt;/p&gt;

&lt;p&gt;The second issue is that it doesn't really give any idea of your experience. I've seen lots of CV tip sites that recommend a list of years of experience but that doesn't always help. Take for example C#. Is that building desktop apps, or web APIs? Maybe it's all in windows services or WCF.&lt;/p&gt;

&lt;p&gt;My current suggestion would be something like the following, but I'd love to hear/see your ideas 🙂&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Technologies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core Languages&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;C#&lt;/strong&gt; 7+, Asp.Net 4+ (WebApi &amp;amp; MVC), Entity framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core Databases&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;, &lt;strong&gt;MongoDB&lt;/strong&gt;, &lt;strong&gt;T-SQL&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core Tooling&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OctopusDeploy&lt;/strong&gt;, &lt;strong&gt;TeamCity&lt;/strong&gt;, &lt;strong&gt;Fiddler&lt;/strong&gt;, &lt;strong&gt;Postman&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Python&lt;/strong&gt;, Flask, &lt;strong&gt;Ms SQL Server&lt;/strong&gt;, &lt;strong&gt;SQLite&lt;/strong&gt;, &lt;strong&gt;LiteDB&lt;/strong&gt;, &lt;strong&gt;AWS&lt;/strong&gt; (EC2, X-Ray S3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's always a good idea to link any experience you mention in the summary with your job roles and include context, like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this role, I was a &lt;strong&gt;key architect&lt;/strong&gt; in developing &lt;strong&gt;large scale&lt;/strong&gt; C# (&lt;strong&gt;8.0, .net 4.7.2&lt;/strong&gt;) apps with PostgreSQL and entity framework.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;This doesn't apply to all roles as it may be useful for a graphic designer position for example but if you're applying for a developer/tester/sys ops type role don't focus too much on the design. Stand out by your clear and obvious thought when preparing your CV by keeping the content clear and easy to read. You don't need colours, graphics or charts to sell yourself, they're just a distraction. I'm not saying make it ugly, just don't overthink it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bonus
&lt;/h2&gt;

&lt;p&gt;You're in this industry (hopefully) because you love tech. You probably have personal projects you work on outside of your normal job. It's always a real bonus to get to see what candidates have worked on in their spare time, throw that GitHub or personal website link in there or in your covering letter.&lt;/p&gt;

&lt;p&gt;So that's it, I hope you found some of it useful. Some of the points may seem a little obvious, but they're all based on things I've seen frequently.&lt;/p&gt;

&lt;p&gt;I'd love to know if you agree with any of these thoughts, or maybe you have some of your own tips and tricks you'd like to share? ♥&lt;/p&gt;

</description>
      <category>resume</category>
      <category>career</category>
      <category>interview</category>
      <category>cv</category>
    </item>
  </channel>
</rss>
