{"schemaVersion":1,"articles":[{"slug":"composing-reusable-ai-workflows","title":"Composing reusable AI workflows","date":"2026-09-14","updated":"2026-09-14","tags":["AI","skills","plugins"],"url":"https://www.rasmusolsson.dev/posts/composing-reusable-ai-workflows/","excerpt":"Several of my AI workflows need to do similar things: read a ticket and its requirements, understand the relevant code, check the working environment, load company conventions, pla...","content":"\nSeveral of my AI workflows need to do similar things: read a ticket and its requirements, understand the relevant code, check the working environment, load company conventions, plan a change, review its behavior or run the relevant tests. Investigations need to resolve the right environment and correlate evidence. Delivery workflows need to assess feedback, update a PR description and leave enough context for the work to continue later.\n\nThose needs overlap. Implementing a feature and fixing a review comment both need validation after code changes. Reviewing a plan and reviewing a PR can both benefit from an architectural perspective. A RabbitMQ investigation may need the same log-searching workflow that I use directly. Putting all of those instructions into every skill would leave me maintaining several versions of the same procedure.\n\nInstead, I want to compose workflows from smaller parts. A shared review step can support both implementation and a standalone PR review. An agent can bring a particular perspective wherever that perspective is useful. A script can handle a repeatable operation without asking the AI to reconstruct it each time.\n\nMy `raholsn` plugin brings those pieces together. The individual posts in this series describe what each workflow does. This one looks at how they connect, using the plugin's `docs/command-dependencies.md` as a map of the current design.\n\n![Three workflows connected to shared instruction cards, focused role cards and a tray of reusable tools](/images/composing-skills/cover.webp)\n\n## Give each part a clear job\n\nThe implementation described here is a Claude Code plugin. The idea of composing reusable instructions applies more broadly, but command registration, agent configuration and integration files depend on the tool hosting them.\n\nThese are the main pieces in my plugin:\n\n| Part | Purpose | Example |\n|---|---|---|\n| User-facing skill | Coordinates a task I ask the assistant to perform | `work`, `code-review`, `rabbitmq` |\n| Reference skill | Supplies a shared procedure or supporting guidance | `ref-review-loop`, `ref-build-and-test` |\n| Agent | Handles a focused assignment with its own role and context | `architect`, `functional-reviewer`, `qa` |\n| Profile and company guidance | Supply the context that varies between workplaces | Tracker settings, engineering conventions |\n| MCP connection | Exposes external tools and data to the assistant | Elastic, Linear, SQL Server |\n| LSP configuration | Makes language-server support available | C#, Terraform, YAML |\n| Bundled script | Implements a repeatable local operation | `get-deploy-status.sh` |\n\nThe plugin packages the reusable pieces so I can maintain them together. Its current manifest registers 15 commands, alongside 21 internal `ref-*` skills and eight bundled agents. Those counts describe this version, rather than a target for how large a plugin should become.\n\n## What I mean by a reference skill\n\nThe `ref-` prefix is my convention for internal supporting skills. Some describe a procedure, such as validating a change. Others resolve guidance that several agents need, such as the company's testing conventions.\n\nI do not normally invoke these myself. A parent workflow explicitly references the supporting skill when it needs it. That keeps the user-facing command focused on the task while giving the shared instructions a place of their own.\n\nFor example, `ref-build-and-test` describes how to prepare for the selected checks, run them, handle cleanup and report the result. `work` uses it during implementation. `fix-comments` can use it after changing code, and the review loop uses it by default to validate fixes.\n\nIf I improve how that shared procedure preserves a setup failure while also reporting a cleanup failure, the callers can use the updated procedure. I do not need to find and reconcile separate copies inside each workflow.\n\nIn this Claude Code implementation, the reference files use:\n\n```yaml\nuser-invocable: false\ndisable-model-invocation: false\n```\n\nThe first hides the skill from the slash-command menu; the second leaves it available to the model. The rule that it should only be used when another workflow references it is expressed in the instructions. These flags alone do not enforce that caller relationship. The [skill invocation documentation](https://code.claude.com/docs/en/skills#control-who-invokes-a-skill) explains the distinction.\n\n## A skill can delegate to an agent\n\nA reference skill and an agent have different responsibilities. The reference describes how a step fits into the workflow: what input it needs, who should do the work and what result the caller expects. The agent carries out the focused assignment.\n\nFor example, `ref-architect-review` passes the task brief and relevant source or diff to the `architect` agent. The agent examines boundaries, contracts, concurrency and failure behavior, then writes architectural feedback. The calling workflow decides how to apply that feedback.\n\nThe same architecture perspective can be useful before implementation or while reviewing an existing PR. What changes is the supplied task and evidence.\n\nThe functional reviewer has a different focus: whether the change delivers the intended behavior and user journey. QA examines the evidence that it works. Keeping those roles explicit helps the caller choose the perspectives that matter, then reconcile overlapping findings.\n\nAgents also need shared context. Several of mine preload `ref-company-conventions` and `ref-company-testing` through their `skills` frontmatter. I configure the current company’s guidance directly in those shared references. That is context supplied to the agent, rather than another agent being launched. Claude Code documents these preloads in its [subagent configuration reference](https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields).\n\n## Reuse the review step in two different workflows\n\nThe review loop is a useful example of composition because its callers need different behavior.\n\nDuring implementation, `work` can ask for a review, address supported findings, validate the fixes and review again when the changes warrant it. A standalone `code-review` request needs findings about the supplied PR; it should not begin modifying that change as part of the review.\n\nBoth use `ref-review-loop`, with an explicit mode:\n\n| Caller | Mode | Result |\n|---|---|---|\n| `work` | `loop` | Review, address findings, validate changes and repeat when useful |\n| `code-review` | `single-pass` | Review once and return findings |\n\nThe reference also accepts the reviewer, feedback-file path, diff source and validation call. Its defaults are the functional reviewer and the shared build-and-test procedure.\n\nThat makes the common procedure reusable while leaving the caller in control of the task. The input matters: a saved PR diff must not be replaced by whatever happens to be in the local working tree.\n\nThis simplified diagram shows those relationships. Arrows identify dependencies, not the order of every action in a run.\n\n![Work and code review share a review procedure, which delegates to a functional reviewer and uses validation after loop-mode fixes](/diagrams/composing-skills/overview.svg)\n\n[Open the review example at full size](/diagrams/composing-skills/overview.svg)\n\nShared artifacts make the handoff concrete. A task brief records the scope and requirements, the reviewer produces feedback in an agreed location, and the caller knows where to find it. Reuse depends on that agreement about inputs and outputs as much as it depends on shared wording.\n\n## Commands can compose other commands too\n\nComposition does not always need a reference skill or an agent. The [RabbitMQ workflow](/posts/investigating-rabbitmq-queues-with-ai) resolves its connection through `profile` and can call `logs` when it has relevant identifiers and a configured log connection.\n\nThat lets the RabbitMQ skill concentrate on broker evidence while the [logs skill](/posts/following-the-evidence-through-logs-with-claude) owns the log investigation. Improvements to log querying and correlation can be reused without copying that method into the broker workflow.\n\nSome relationships are simply handoffs between tasks. The planning sequence is:\n\n```text\ngrill-me-pragmatic → write-requirements → plan-implementation → work\n```\n\nI choose when to move between those stages. Producing a requirements document does not automatically run issue planning or start implementation. That is why the dependency guide distinguishes a suggested workflow sequence from an actual instruction to invoke another skill.\n\n## The tools folder handles repeatable operations\n\nThe plugin also contains shell scripts under `tools/`. Four small commands directly invoke matching scripts for deployment status, opening a PR-related page, opening a solution and opening an Argo CD application.\n\nFor example, `get-deploy-status` resolves the repository, workflow and job filter, then passes them to `get-deploy-status.sh`. The script reads GitHub Actions results and returns statuses and links. It does not establish live application health.\n\nThe split is useful: the skill interprets the request and resolves the arguments; the script performs the defined operation and reports its result. If the script's argument contract stays the same, I can change its implementation without rewriting how the skill understands the request.\n\nThese small commands do not need the whole `work` workflow or profile setup. There is also a `create-branch-and-pr.sh` helper for the combined operation supported by its reference skill. Its presence in the plugin does not mean every workflow runs it: `work` uses that reference for naming guidance without invoking the combined helper.\n\n## MCP and LSP provide capabilities\n\nThe current `.mcp.json` declares connections for Elastic, Linear and SQL Server. These expose capabilities that workflows can use: querying logs, reading tracker context or accessing the configured database tools.\n\nA connection still needs the appropriate endpoint, authentication and permissions. Declaring it in the plugin does not establish access. The workflow decides which available tools are relevant to the request and what to do with their results.\n\nThe `.lsp.json` file configures language servers for C#, Terraform and YAML. I do not use that part much yet, but I like having language-server support available when it is useful for code navigation or diagnostics. The host and installed server determine which operations are available; I am not claiming every workflow currently uses them.\n\nThis setup still has practical portability work left. The current C# configuration contains machine-specific paths, and the configured language-server binaries need to be installed. Packaging the configuration is only part of making it usable elsewhere. The [Claude Code plugin reference](https://code.claude.com/docs/en/plugins-reference) describes the MCP and LSP integration points.\n\n## Keep workplace context separate\n\nIn [Making skills useful across workplaces](/posts/making-claude-plugins-easier-to-reuse), I described using a profile and optional company pack for settings and guidance that change between companies.\n\nThat separation also helps composition inside the plugin. The architect, planner and functional reviewer all use the same company reference skills. I configure conventions and testing guidance directly in those references for the current assignment, keeping their names stable when moving to the next company. The agents do not need their own copies of that guidance.\n\nIf a company updates its testing guidance, I update the shared testing reference. An external document can still be selected when useful, but a separate pack is optional. If I improve how a review is coordinated, the change belongs in the workflow. If a connection moves, its configuration changes. Giving those changes different homes makes it easier to see what needs updating and which callers may be affected.\n\n## Use the dependency map when changing shared behavior\n\nThe full chart below is copied from `docs/command-dependencies.md`. It shows commands, references, agents and bundled script dependencies in the current source. It is a maintenance map, not a record of which steps ran during a particular task.\n\n<details>\n<summary>Explore the full plugin dependency map</summary>\n\nArrows point from callers to their dependencies. Thick arrows connect commands; solid arrows show reference use or agent delegation. Dashed agent arrows show preloaded context, and the naming-only reference from `work` is explicitly labelled. Conditional labels explain when a relationship applies. Configured domain reviewers sit outside the fixed set of bundled agents.\n\nThe chart is large; open the full-size version to inspect individual connections.\n\n![Full source dependency map of the raholsn plugin's commands, reference skills, agents and bundled script commands](/diagrams/composing-skills/dependencies.svg)\n\n[Open the full dependency map](/diagrams/composing-skills/dependencies.svg)\n\n[Download the Mermaid source](/diagrams/composing-skills/dependencies.mmd)\n\n</details>\n\nCentralizing a procedure also means a change can affect several workflows. Updating the default validation in the review loop deserves a check of its callers, especially the difference between loop mode and single-pass review. Changing a feedback format means checking both the agent that writes it and the caller that reads it.\n\nI want the graph to make those dependencies visible. It needs updating when the source relationships change; a diagram that drifts away from the instructions can be misleading.\n\nThere is a balance to the decomposition too. A separate reference is useful when it has a clear responsibility and meaningful reuse. Splitting every paragraph into another file would make the workflow harder to follow. Shared instructions reduce duplication, but they still need clear boundaries and validation in the workflows that use them.\n\n## The plugin and dependency guide\n\nYou can find [my plugin](https://github.com/raholsn/raholsn-claude-plugin) and the [command dependency guide](https://github.com/raholsn/raholsn-claude-plugin/blob/main/docs/command-dependencies.md) on GitHub.\n\nThis post describes the local plugin structure as of its updated date. The diagram reflects source relationships; it does not establish that every integration or workflow has been exercised live.\n\nFor me, the benefit is being able to improve a shared piece and use it in several places, while keeping each workflow understandable. Skills describe the method, agents contribute focused work, and tools provide the operations. Composing them gives me more useful workflows with fewer copies of the same instructions to maintain.\n\nHappy coding!\n"},{"slug":"investigating-rabbitmq-queues-with-ai","title":"A natural language interface for RabbitMQ","date":"2026-08-14","updated":"2026-09-14","tags":["AI","skills","observability"],"url":"https://www.rasmusolsson.dev/posts/investigating-rabbitmq-queues-with-ai/","excerpt":"A queue is growing, exports are waiting, and the service appears to be running. There are several places to look: queue counts, consumers, connections, routing and application logs...","content":"\nA queue is growing, exports are waiting, and the service appears to be running. There are several places to look: queue counts, consumers, connections, routing and application logs. The challenge is connecting those observations well enough to explain what is happening.\n\nI want the AI to help gather that evidence and identify the next useful check. My `rabbitmq` skill gives the investigation a structure, starting with broker metadata and following the evidence into logs when needed.\n\n![Queued message envelopes under a magnifying glass, with two observation sheets and an empty workstation at the end of the queue](/images/rabbitmq/cover.webp)\n\n## Start with a specific question\n\nThe skill can investigate a named queue, survey backlogs, examine dead-letter routing or check broker health. This illustrative example uses the Claude Code command for my implementation:\n\n```text\n/raholsn:rabbitmq sandbox why has the exports queue stopped making progress?\n```\n\nIf I do not yet know which queue needs attention, I can start more broadly:\n\n```text\n/raholsn:rabbitmq sandbox list queues with a backlog\n```\n\nThe first question goes directly to the named queue. The second uses a bounded, paginated survey and then checks interesting queues individually. The answer should identify how much of the requested scope was inspected, including any limits on the survey.\n\nBefore connecting, the skill resolves the environment, management endpoint and virtual host, or vhost. A queue name alone is not enough to identify the target across environments and vhosts. Those details should be visible in the answer so I can check where the evidence came from.\n\n## Compare observations over time\n\nFor a progress question, the skill captures at least two timestamped observations. It looks at ready and unacknowledged message counts, consumers, delivery and acknowledgement metrics where available, and relevant connection or channel state.\n\nSuppose our illustrative export queue produces these snapshots:\n\n| Observation | 09:00 UTC | 09:01 UTC |\n|---|---|---|\n| Ready messages | 120 | 145 |\n| Unacknowledged messages | 0 | 0 |\n| Consumers | 0 | 0 |\n\nThe backlog grew between observations, and neither snapshot showed a registered consumer. That gives us a concrete direction: establish whether the export consumer was expected to be active, then inspect its lifecycle and logs.\n\nIt does not yet explain why the consumer is absent. The service could be intentionally stopped, starting up, failing to subscribe or handling a cancellation incorrectly. Two snapshots also do not describe everything that happened between them.\n\nIf consumers were present and messages remained unacknowledged, the next questions would be different. We would need to examine delivery and acknowledgement activity alongside the application's processing time. A stable queue count alone would not tell us whether work was moving through it.\n\nMissing metrics stay unknown. The assistant should not turn an absent field into a zero, or an unsuccessful API request into an empty queue.\n\n## Keep the evidence before changing the system\n\nA restart may change the state we are trying to understand. I want the queue counts, consumer details, relevant policies and observation times captured before discussing recovery.\n\nThe skill records the broker version too. It uses management snapshots as evidence with a time and scope, without treating them as an atomic view of the whole system. For broker-health questions, it checks accessible alarms, partitions and resource pressure. A responding management API is only one part of that picture.\n\n### The investigation workflow\n\nThese diagrams describe the current skill instructions, not a recorded investigation.\n\n![RabbitMQ investigation from target resolution through metadata, optional message inspection and evidence correlation](/diagrams/rabbitmq/overview.svg)\n\n[Open the overview at full size](/diagrams/rabbitmq/overview.svg)\n\n<details>\n<summary>Explore metadata collection, message inspection and log correlation</summary>\n\nThe sequence shows where the skill compares observations, when message inspection needs a decision and how it follows relevant identifiers into logs.\n\n![Detailed RabbitMQ investigation sequence including connection setup, bounded reads, optional sampling and cleanup](/diagrams/rabbitmq/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/rabbitmq/sequence.svg)\n\n</details>\n\n## Follow dead-letter routing before naming the cause\n\nA queue called `exports.dead` is a useful candidate to investigate. The skill checks source queue arguments, policies and bindings to establish how messages reach it. The name is a convention, and applications may also publish directly to an error queue.\n\nWhen an authorized message sample contains `x-death` headers, those can establish the source queue, exchange, reason, count and timestamps of broker dead-lettering. Reasons include rejection, expiry, exceeding a queue-length limit and reaching a quorum queue's delivery limit. RabbitMQ documents these fields in its [dead-lettering reference](https://www.rabbitmq.com/docs/dlx).\n\nThose fields describe a broker outcome. They do not supply the application exception or establish that a particular retry library exhausted its attempts.\n\nIf the evidence provides a correlation ID and logs are configured, the skill can hand that identifier and a bounded UTC window to the [logs workflow](/posts/following-the-evidence-through-logs-with-claude). The header conventions come from the configured company notes or observed data.\n\nFor the export example, a matching application trace might explain what the worker was doing before it stopped making progress. If log access is unavailable, the broker findings still stand, while the application explanation remains unverified.\n\n## Fetching a message is a separate operation\n\nSometimes metadata and logs are enough. When the investigation needs a message sample, there is an additional decision to make.\n\nThe current implementation uses the management API's message-fetch endpoint with `ack_requeue_true`. That requests requeue after fetching, but it changes queue state. RabbitMQ describes this endpoint as intended for development and troubleshooting, and advises using messaging or streaming protocol clients in production. See the [HTTP API reference](https://www.rabbitmq.com/docs/http-api-reference#post--api-queues-vhost-name-get).\n\nThe skill checks the queue type, broker version and effective delivery-limit policy before proposing a sample. Requeue should not be presented as a passive peek or a guarantee that delivery state will remain unchanged. Version matters here: RabbitMQ 4.3 changed which returns count toward quorum delivery limits, as explained in its [poison-message handling documentation](https://www.rabbitmq.com/docs/quorum-queues#poison-message-handling).\n\nWhen inspection has not already been authorized within those bounds, the proposal names the exact target, count, payload truncation and fields to display, and explains the effects. I can choose **Inspect**, **Metadata only** or **Cancel**. Metadata only continues the investigation; Cancel ends it.\n\nThe defaults are a maximum of five messages and payload truncation at 4096 bytes. Displayed fields are limited separately. Fetching still retrieves payload data even if the assistant only returns selected metadata, so hiding the body in the answer does not make it a metadata-only request.\n\nAn uncertain fetch is not automatically retried. Repeated requeue samples may return the same messages, so they are not a way to page through the queue either.\n\n## Supply the connection through the profile\n\nThe [company profile](/posts/making-claude-plugins-easier-to-reuse) supplies RabbitMQ environments, management URLs, vhosts and the path to a protected authentication file. Credentials stay outside the profile and repository.\n\nThis implementation uses `curl` and `jq` against the management HTTP API. Basic or OAuth authentication uses the locally provisioned auth file; any company-specific login procedure belongs in the configured notes. There is no additional RabbitMQ MCP server bundled with the skill.\n\nSome environments need a Kubernetes port-forward. The profile can provide that connection information, and the skill checks the tunnel before using it. It cleans up only a tunnel it started itself. Kubernetes is not required for a directly accessible endpoint.\n\nThis keeps the investigation method reusable while the workplace supplies the access details and queue conventions it cannot reliably infer.\n\n## Finish with findings and the next useful action\n\nThe result should make clear what was inspected, when it was observed and what the evidence supports. It includes any sampling scope, correlated logs, missing information and remaining hypotheses. By default, that is a conversational answer; a redacted report is saved when requested.\n\nThe investigation can inform recovery, but replay needs more context than a failed message. For an export, we may need to establish whether an earlier attempt already created a file or sent a notification, how duplicate work is handled and where the message should go. A transient error alone does not settle those questions.\n\nThe skill does not execute replay, purge, policy changes or restarts. It provides the evidence and a proposed next action for the operational workflow responsible for recovery.\n\n## The skill\n\nYou can find the [`rabbitmq` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/rabbitmq/rabbitmq.md) on GitHub.\n\nThis article describes the workflow as of its updated date. The export queue and counts are illustrative, not results from a live broker investigation.\n\nFor me, the value is having help connect broker state to application behavior, with enough evidence to check the explanation. A useful answer can narrow the problem and show what to investigate next, even when the root cause is still open.\n\nHappy coding!\n"},{"slug":"the-token-economy-of-an-engineering-team","title":"The token economy of an engineering team","date":"2026-08-01","tags":["AI","engineering","productivity"],"url":"https://www.rasmusolsson.dev/posts/the-token-economy-of-an-engineering-team/","excerpt":"Updated September 15, 2026 with current pricing and provider reporting. For many teams, working with AI agents is already the default way to develop software. Most of the engineeri...","content":"\n*Updated September 15, 2026 with current pricing and provider reporting.*\n\nFor many teams, working with AI agents is already the default way to develop software. Most of the engineering teams I hear from work this way rather than writing code by hand. Some run several in parallel, implementing a change while another agent investigates a problem or reviews code.\n\nWe are also creating reusable skills and workflows that make this way of working more manageable. As those workflows improve, it becomes easier to give agents more responsibility. From these conversations, adoption still seems to be growing rapidly, both in how often people use agents and how much work they trust them with.\n\nThe focus is often on faster delivery and room to experiment. Token consumption gets less attention, especially when subscriptions keep the bill predictable. Within a plan's limits, a busy day with several agents can cost the same as a quiet one, even though the compute behind it looks very different.\n\nBut what would that activity cost if every token were metered? The difference between subscription and API pricing offers a useful way to explore how growing AI usage could affect an engineering team's budget.\n\n![Parallel engineering workflows connected to a usage meter, with subscription cards and a budgeting notebook on a desk](/images/ai-token-economy/cover.webp)\n\n## What AI usage could cost a team of 30\n\nTo put numbers to this, consider a team using Claude with Opus and Codex with GPT-5.6 Sol. Having both subscriptions gives developers another option when one tool reaches its limit. Usage can grow within those allowances without changing the subscription bill, although neither plan guarantees a fixed daily token count.\n\nFor an illustrative team of 30, [Claude Max 20x](https://support.claude.com/en/articles/11049741-what-is-the-max-plan) and the [$200 ChatGPT Pro tier, including Codex](https://help.openai.com/en/articles/9793128-what-is-chatgpt-pro), come to **$400 per developer per month**.\n\nThat is **$12,000 per month**, or **$144,000 per year**, before taxes and extra usage. This assumes existing individual subscriptions: OpenAI paused new purchases and upgrades to Pro $200 on September 10.\n\nTo get a sense of the volumes involved, I asked developers about their average daily token consumption. Those conversations led me to a rough working estimate of **50 million uncached input tokens and 200,000 output tokens per developer per day**, across both tools. Usage varied considerably, so this is an informal estimate rather than a measured industry average. It gives us a starting point for exploring the cost of heavy agent usage.\n\nUsing 20 working days per month, we can compare the subscription bill with published API prices.\n\n| Model | Per million uncached input tokens | Per million output tokens |\n|---|---:|---:|\n| Claude Opus 5 | $5 | $25 |\n| GPT-5.6 Sol | $4 | $20 |\n\nRates checked September 15, 2026: [Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing) and [Sol pricing](https://developers.openai.com/api/docs/models/gpt-5.6-sol). Sol's standard short-context rates are promotional through at least November 21, 2026.\n\n| Scenario | Per developer / month | 30 developers / month |\n|---|---:|---:|\n| Both subscriptions | $400 | $12,000 |\n| Sol API rates | $4,080 | $122,400 |\n| Opus API rates | $5,100 | $153,000 |\n\nEach API row shows what it would cost to use that model for all the estimated work. Using a mix of models would change the total.\n\nThe calculation assumes the output estimate includes [reasoning tokens](https://developers.openai.com/api/docs/guides/reasoning#how-reasoning-works). The figures exclude caching costs, tool fees and taxes. Discounts or extra charges for very long requests could also affect the bill.\n\nThat is roughly **10–13 times the subscription bill** at these model prices. It shows how differently the same usage can be priced, rather than what teams should expect to pay in future.\n\nThe gap can make subscriptions feel heavily subsidised. But API prices do not tell us what providers spend serving requests, so this comparison cannot show how much they earn or lose on a subscription.\n\n[Reporting on OpenAI's paid-product compute margin](https://www.pymnts.com/artificial-intelligence-2/2025/openai-compute-margin-doubles-over-nearly-two-years/) suggests that serving AI can carry substantial margins. That measure covers paid products broadly; it does not establish the API margin on these models or overall company profitability.\n\n## Competition and cheaper models\n\nThe comparison uses two premium models. Teams also have much cheaper options: Google launched [Gemini 3.1 Flash-Lite at $0.25 per million input tokens and $1.50 per million output tokens](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-1-flash-lite/). A lower-priced model will not suit every coding task, but teams can benefit wherever it delivers the quality they need.\n\nThat choice matters. As alternatives become more capable, Anthropic and OpenAI face pressure to keep their offerings attractive through price, performance and usage allowances. I expect that competition to benefit customers, even if providers respond in different ways.\n\nThere is also a history of falling costs. [Stanford's 2025 AI Index](https://hai.stanford.edu/news/ai-index-2025-state-of-ai-in-10-charts) reported a more than 280-fold price decline for GPT-3.5-level performance between November 2022 and October 2024. That does not predict future frontier-model prices, but better hardware and more efficient models could continue to make useful capability cheaper.\n\nPricing and allowances are worth keeping in mind as workflows grow. There is no need to assume a sudden price increase to make token economics relevant: cheaper models can encourage more usage too. **If token prices halve while usage triples, the metered bill still rises by 50%.**\n\n## Budget for useful work\n\nAs engineering teams become better at working with agents, token consumption is likely to keep growing. Cheaper models and stronger competition can make that growth more affordable, while opening up more work worth handing to AI.\n\nFor now, subscriptions can make extensive usage a relatively predictable expense. Over time, the choice of model, reasoning effort and number of parallel agents may become a more visible part of engineering budgets.\n\nThat leaves teams with a useful question: what are they getting back from the extra usage? Faster delivery, better software and more room to explore ideas can make it worthwhile. The token count alone cannot tell that story.\n\nHappy coding!\n"},{"slug":"turning-requirements-into-implementation-issues","title":"Breaking requirements into manageable issues","date":"2026-07-23","updated":"2026-09-14","tags":["AI","skills","planning"],"url":"https://www.rasmusolsson.dev/posts/turning-requirements-into-implementation-issues/","excerpt":"A requirements document gives us something to build against. There is still a planning step between that document and picking up the first issue: deciding how to divide the work, w...","content":"\nA requirements document gives us something to build against. There is still a planning step between that document and picking up the first issue: deciding how to divide the work, what depends on what, and which decisions need attention before implementation can start.\n\nThat is where I use the `plan-implementation` skill. It turns a PRD or clarified plan into a local issue breakdown that I can review before creating anything in a tracker.\n\nMatt Pocock authored the original `to-prd` and `to-issues` skills and workflow. I have modified them for how I want to work, including the local planning record and the publication checks described here. Credit for the originals belongs to Matt; you can explore his work in [Matt's skills repository](https://github.com/mattpocock/skills). My adaptations are now named `write-requirements` and `plan-implementation`. This article covers my version as of its updated date.\n\n![A requirements sheet connected to small implementation cards, with a pending decision among the dependencies](/images/to-issues/cover.webp)\n\n## Start from the requirements\n\nIn [Turning planning decisions into requirements](/posts/turning-planning-decisions-into-requirements), I described preserving the decisions from planning in a PRD with stable requirement and acceptance-criterion IDs. Those references give the issue breakdown something concrete to cover.\n\nThis illustrative invocation uses the Claude Code command for my implementation:\n\n```text\n/raholsn:plan-implementation docs/prds/2026-08-06-export-retry-prd.md\n```\n\nThe skill can also use a clarified plan directly or read a supplied tracker reference when access is available. If the intended source is unclear, it asks which one to use. The newest file is not necessarily the right plan.\n\nThe source sets the scope. Splitting a requirement into issues should preserve the agreed behavior and non-goals, including any unresolved decisions. Relevant code can help establish existing contracts and dependencies, but an implementation guess should not become a new product requirement along the way.\n\n## Give each issue an observable result\n\nContinuing the illustrative export-retry example, the previous post used this requirement:\n\n> Retrying an eligible failed export uses the parameters saved with the original request.\n\nA useful first implementation slice could take one agreed class of eligible failure through the complete retry path: the user requests a retry from the dashboard, the application creates the new attempt with the original parameters, and the resulting export can be checked.\n\nThat is a vertical slice. It crosses the layers needed to demonstrate a narrow behavior. Separate tickets for a database change, an endpoint and a button may describe necessary work, but each leaves the intended behavior waiting on the others before we can try it.\n\nThe skill prefers a first slice that delivers value or proves the riskiest integration. Sometimes separate enabling work is necessary; the breakdown should explain why it cannot reasonably fit inside a useful slice.\n\nFor our example, an issue excerpt might look like this:\n\n```text\nI-02: Retry an eligible failed export with its saved parameters\n\nCovers: R1, AC1\nScope: The agreed first failure category, through the dashboard and export flow.\nNon-goal: Editing report parameters during retry.\n\nAcceptance criterion:\nGiven an eligible failed export and a user permitted to retry it,\nwhen the user requests a retry from the dashboard,\nthe new attempt uses the original saved report parameters.\n```\n\nThis is a provisional excerpt, not a complete issue. The earlier example left eligibility and permissions unresolved. Those decisions still need answers before this implementation can be ready.\n\n## Separate missing decisions from dependencies\n\nThe skill uses two classifications to describe how much human input an issue needs:\n\n| Type | Meaning |\n|---|---|\n| **AFK**, away from keyboard | Decisions and acceptance criteria are sufficient for implementation without further human clarification, once prerequisites are satisfied. |\n| **HITL**, human in the loop | A human or external decision is needed. The issue names the question and the expected decision record. |\n\nReadiness is a separate field. An AFK issue can be blocked by another implementation issue. A HITL decision task can be ready for someone to address immediately.\n\nFor the unresolved retry example, `I-01` could be a HITL task to establish the eligible failures and permission rules, with the agreed rules recorded as its outcome. `I-02` would remain HITL and blocked while those decisions affect its behavior. After they are resolved and its acceptance criteria are updated, it could become AFK. Any remaining technical prerequisites would still determine whether it is ready.\n\nCreating `I-01` in a tracker does not resolve the decision. Its dependent issue needs the answer, not just a ticket number.\n\nAFK also does not mean that implementation has been authorized or that the result will be correct. It describes the planning state. I still need to choose the work to start and review what is built.\n\n## Check that the requirements survived the split\n\nA collection of plausible issues can still leave part of the PRD uncovered. The skill maps every in-scope requirement and acceptance criterion to stable local issue IDs.\n\nFor the small excerpt above, the coverage record could include:\n\n| Source | Issue | Remaining gap |\n|---|---|---|\n| R1: Preserve the original report parameters | I-02 | Eligibility and permissions await I-01. |\n| AC1: Verify that the retry uses the saved parameters | I-02 | The criterion needs the agreed eligibility and permission rules. |\n\nThis makes omissions easier to inspect. A proposed deferral stays visible until it is resolved; the assistant should not call the plan complete while silently dropping a requirement.\n\nThe dependency check looks for missing references and cycles, as well as the reason each dependency exists. The breakdown should also make clear who owns integration across issues. Two individually sensible tickets can still leave the connection between them unspecified.\n\nTesting, migration, rollout and operational work belong in the breakdown where the source requires them. I want those requirements carried forward without adding a generic set of tickets to every plan.\n\n### The issue-planning workflow\n\nThese diagrams describe the current skill instructions, not a recorded run.\n\n![Issue planning from source resolution through vertical slices, local saving and optional approved publication](/diagrams/to-issues/overview.svg)\n\n[Open the overview at full size](/diagrams/to-issues/overview.svg)\n\n<details>\n<summary>Explore planning, publication and recovery</summary>\n\nThe sequence includes reviewing the saved draft, preparing tracker operations and reconciling a partially published batch.\n\n![Detailed sequence for drafting issues, reviewing publication and recording confirmed results after tracker writes](/diagrams/to-issues/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/to-issues/sequence.svg)\n\n</details>\n\n## Review the breakdown locally\n\nThe first output is a Markdown file. For this example, it could be:\n\n```text\ndocs/issues/2026-09-11-export-retry-issues.md\n```\n\nI can choose the path. Otherwise the skill uses an existing issue-planning directory, falling back to `docs/issues/`. It preserves existing files unless I request a revision, and leaves the source PRD intact.\n\nThe saved breakdown includes the source, scope, issue bodies, coverage map and dependencies. The response summarizes the issue count, AFK/HITL split, readiness and remaining gaps. Stable IDs such as `I-02` make it possible to discuss and revise an issue without losing its references.\n\nThis part needs no company profile or tracker. A local plan is a useful result on its own, including a provisional plan that clearly shows what remains blocked.\n\n## Publish the plan when it is ready for the tracker\n\nWhen I request publication, the skill uses the [company profile](/posts/making-claude-plugins-easier-to-reuse) to resolve the tracker connection and defaults. It validates the target and fields, checks for duplicates and prepares the exact proposal for review.\n\nI can choose **Publish**, **Edit** or **Cancel**. The proposal includes the issue bodies, status and other fields, plus any dependency links or follow-up updates. Approving the local breakdown alone does not publish it.\n\nPublished issues need enough context to stand on their own. A local PRD path can record where the requirements came from, but someone reading a tracker ticket may not have that file.\n\nAn explicitly approved planning backlog can include blocked issues. Their status should say so. If the tracker cannot represent native blocker links through the available tools, the proposal can use explicit references in the bodies and explain that limitation.\n\n## Keep track of partial publication\n\nPublishing several issues introduces another practical problem: a batch can stop halfway through.\n\nThe skill creates issues in dependency order and records each confirmed tracker ID and URL against its local ID. If a write fails or returns an uncertain result, it stops and uses read-only lookups to establish what exists.\n\nFor example, if two issues were confirmed and the third request timed out, retrying the whole batch could create duplicates. The saved record should show the two confirmed issues, the uncertain third result and the operations still remaining. Resuming requires reconciliation and fresh approval of that remaining work.\n\nThat record is part of the value of saving the breakdown before publishing. It connects the plan we reviewed to the items that were actually created.\n\n## The skill and the next piece of work\n\nYou can find the [`plan-implementation` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/plan-implementation/plan-implementation.md) on GitHub.\n\nThis completes the planning path from [challenging the idea](/posts/letting-claude-challenge-the-plan), through requirements, to issues small enough to build and verify. When I choose a ready implementation issue, the [work skill](/posts/giving-claude-a-workflow-for-implementation) can take it forward. Creating the backlog does not start that step automatically.\n\nFor me, the useful result is a clear next piece of work, with enough context to understand it and enough honesty about what still needs deciding. The AI helps organize that breakdown; I need to check that the slices make sense and still add up to the outcome we wanted.\n\nHappy coding!\n"},{"slug":"making-a-static-blog-accessible-with-mcp","title":"Making a static blog accessible with MCP","date":"2026-07-13","updated":"2026-09-15","tags":["AI","MCP","TypeScript"],"url":"https://www.rasmusolsson.dev/posts/making-a-static-blog-accessible-with-mcp/","excerpt":"I do not expect many people to use a dedicated MCP server for this blog. But it was a fun project, and it makes a small example you can follow if you want to expose your own conten...","content":"\nI do not expect many people to use a dedicated MCP server for this blog. But it was a fun project, and it makes a small example you can follow if you want to expose your own content through MCP.\n\nThe server lets an AI assistant search the posts and read an article inside a conversation. Asking it to find posts about parallel agents is one way to try it out.\n\nThe website still runs as a static Next.js export. The MCP server runs on the user's computer and fetches the published articles over HTTPS, so there is no extra backend behind the blog. This post walks through the pieces, from generating the article feed to connecting the server to Claude Code.\n\n![A laptop connected to a wooden catalogue of article cards, with a magnifying glass resting on an open page](/images/blog-mcp/cover.webp)\n\n## A local server with a remote source\n\nClaude Code starts the MCP server as a program on your computer. They exchange tool calls and results over **stdio**, short for standard input and standard output. The server fetches articles from the website over HTTPS.\n\n```text\nClaude Code\n    ↕ MCP messages over stdio\nLocal blog MCP server\n    ↓ HTTPS request\nStatic blog: /articles.json\n```\n\nThe website only serves a JSON file. The local program handles MCP using the [TypeScript SDK](https://ts.sdk.modelcontextprotocol.io/server).\n\n## Give the articles a stable format\n\nThe blog already reads Markdown files to build its pages. I reused that code to generate an [article feed](https://www.rasmusolsson.dev/articles.json) containing the published posts.\n\nHere is an example entry:\n\n```json\n{\n  \"schemaVersion\": 1,\n  \"articles\": [{\n    \"slug\": \"example\",\n    \"title\": \"An example article\",\n    \"date\": \"2026-09-16\",\n    \"tags\": [\"AI\"],\n    \"url\": \"https://www.rasmusolsson.dev/posts/example/\",\n    \"excerpt\": \"A short introduction.\",\n    \"content\": \"The full article in Markdown.\"\n  }]\n}\n```\n\nThe feed contains the same posts as the website. The server checks `schemaVersion` before reading it. New posts appear after a normal blog deployment, without an npm package update.\n\n## Expose tools\n\nThe server offers three operations:\n\n| Tool | What it returns |\n|---|---|\n| `list_articles` | Titles, excerpts and links, optionally filtered by tag |\n| `search_articles` | Matching articles ranked by keyword relevance |\n| `get_article` | The full Markdown content and link for a selected slug |\n\nThe assistant searches first, then reads the posts it needs. List and search return short summaries in small batches.\n\nEach tool has a description, rules for its inputs and a function that handles the request. These tell the assistant what the tool does and how to call it.\n\nHere is a shortened version of the reading tool. It assumes `server`, `store` and Zod (`z`) are already set up:\n\n```ts\nserver.registerTool(\n  \"get_article\",\n  {\n    description: \"Read a blog article using a slug returned by search.\",\n    inputSchema: { slug: z.string().trim().min(1) },\n  },\n  async ({ slug }) => {\n    const article = (await store.load()).find(a => a.slug === slug);\n\n    return {\n      isError: !article,\n      content: [{\n        type: \"text\",\n        text: article\n          ? JSON.stringify(article)\n          : \"Article not found. Search for a valid slug first.\",\n      }],\n    };\n  }\n);\n```\n\nThe [full implementation](https://github.com/raholsn/blog-mcp/blob/master/src/server.ts) also handles download errors. All three tools are read-only.\n\nThe server checks the downloaded data and caches it for five minutes. Search matches keywords in titles, tags and article text. Title and tag matches rank higher.\n\n## Connect it to Claude Code\n\nThe server is distributed as the npm package `@raholsn/blog-mcp`. With Node.js 22.14 or newer and Claude Code installed, run:\n\n```sh\nclaude mcp add --transport stdio --scope user rasmus-blog \\\n  -- npx -y @raholsn/blog-mcp@latest\n```\n\nClaude Code launches the server for you. `--scope user` makes it available across projects. Use `/mcp` inside Claude Code to check the connection. [Configuration reference](https://code.claude.com/docs/en/mcp).\n\nThen ask:\n\n> Find Rasmus's articles about parallel agents, then read the most relevant post.\n\nClaude can use the tools directly, without a separate skill. Other MCP clients that support stdio can also run the server.\n\n## Wrapping up\n\nThis was a fun little addition to the blog. If you have been thinking about building an MCP server for your own content, I hope this gives you a few ideas to try. You can find the full example in [raholsn/blog-mcp](https://github.com/raholsn/blog-mcp).\n\nHappy coding!\n"},{"slug":"turning-planning-decisions-into-requirements","title":"Turning planning decisions into requirements","date":"2026-07-02","updated":"2026-09-14","tags":["AI","skills","planning"],"url":"https://www.rasmusolsson.dev/posts/turning-planning-decisions-into-requirements/","excerpt":"A planning conversation can leave us with a much clearer idea of what to build. But the decisions are still spread across questions, answers and corrections. Before breaking the wo...","content":"\nA planning conversation can leave us with a much clearer idea of what to build. But the decisions are still spread across questions, answers and corrections. Before breaking the work into issues, I want a concise document that explains the intended behavior and how we will know it works.\n\nThat is where the `write-requirements` skill fits. It takes the clarified plan and turns it into a product requirements document, or PRD, while keeping the decisions and remaining assumptions visible.\n\nMatt Pocock is the original author of the `to-prd` and `to-issues` skills that inspired this part of my workflow. I have modified them for how I want to work, but credit for the original skills and workflow belongs to him. You can explore his work in [Matt's skills repository](https://github.com/mattpocock/skills). My adaptations are now named `write-requirements` and `plan-implementation`. This article describes my version as of its updated date.\n\n![Planning decisions carried from small cards into a structured requirements document with linked acceptance checks](/images/to-prd/cover.webp)\n\n## Continue from the decisions we already made\n\nIn [Letting AI challenge the plan](/posts/letting-claude-challenge-the-plan), I described using a planning conversation to uncover the decisions that matter. Its output is a decision record: what we agreed, what we assumed and what still needs attention.\n\nThe next step should use that work. I do not want another interview covering the same ground, or a polished specification that quietly changes the decisions we just made.\n\nThe skill reads the supplied record and relevant conversation, then organizes them into requirements. It can also work from a sufficiently clarified plan directly; the previous skill is a useful starting point, not a mandatory ceremony.\n\nThis example uses the Claude Code command for my implementation:\n\n```text\n/raholsn:write-requirements docs/planning/2026-07-02-export-retry-grill-decisions.md\n```\n\nThe path is illustrative. I can also specify an output location or ask it to revise an existing PRD with later decisions.\n\nLocal drafting does not require a tracker or company profile. The useful input is the plan and enough context to represent it accurately.\n\n## Turn a decision into something we can check\n\nContinuing the illustrative report-export example, suppose we agreed that a retry should use the original report parameters. Editing the report would be outside the first version.\n\nThe PRD can express that as a requirement and an acceptance criterion:\n\n| ID | Example |\n|---|---|\n| R1 | Retrying an eligible failed export uses the parameters saved with the original request. |\n| AC1 → R1 | Given an eligible failed export, when the user retries it, the new attempt uses the same saved report parameters. |\n\nThis is a small excerpt, not a complete specification. We still need to establish what makes a failure eligible and who may request a retry.\n\nThe distinction is useful: the requirement states the expected behavior, and the acceptance criterion gives us an observable way to check it. “Retries should work well” would leave both implementation and review open to interpretation.\n\nThe skill gives requirements and acceptance criteria stable identifiers and links them together. When the PRD is revised, those identifiers stay intact. The next planning step can then show which issues cover each requirement instead of relying on similar wording in several places.\n\n## Keep the document proportional to the work\n\nA PRD does not need to become a long document just because a template has many headings. I want enough detail that someone can understand the intended change without rereading the entire conversation.\n\nThe skill captures the problem, goals and non-goals, affected users, requirements, acceptance criteria and relevant implementation decisions. Testing, operational considerations, risks and assumptions belong there when they affect the work.\n\nFor the retry example, preserving the original parameters is a meaningful contract. The name of a private helper method probably is not. Stable behavior and component names are more useful than a list of file paths that may change during implementation.\n\nNon-goals matter too. Explicitly keeping parameter editing outside the first version helps prevent a retry feature from growing into a report editor along the way.\n\n## A missing decision should stay visible\n\nAn AI can make an incomplete plan sound complete. That is one of the things I want this skill to resist.\n\nIf the source does not establish who can retry an export, the PRD should not quietly invent an authorization rule. The skill asks a focused question when the missing answer would materially change behavior, scope or acceptance criteria. It can continue drafting independent sections while that decision is pending.\n\nLow-risk gaps can remain explicit assumptions. Material implementation gaps affect the document's readiness:\n\n| Status | Meaning |\n|---|---|\n| **Ready for issue planning** | Requirements and acceptance criteria are concrete enough to break into work, with any remaining low-risk assumptions stated. |\n| **Blocked** | An unresolved implementation decision affects identified requirements. The saved document remains a draft. |\n\nThat status describes the document. It does not approve implementation or establish that stakeholders have agreed to the requirements.\n\nSome prerequisites only affect rollout. For example, an agreed support briefing before release can be recorded as a release condition without blocking unrelated issue planning. A missing decision about retry permissions changes what we need to build, so it belongs among the implementation blockers.\n\n### The PRD workflow\n\nThese diagrams describe the current skill instructions, not a recorded run.\n\n![PRD workflow from source decisions through drafting and readiness assessment to local saving and optional publication](/diagrams/to-prd/overview.svg)\n\n[Open the overview at full size](/diagrams/to-prd/overview.svg)\n\n<details>\n<summary>Explore drafting and optional publishing</summary>\n\nThe sequence shows how the skill preserves source context, reports unresolved decisions and handles publishing when requested.\n\n![Detailed sequence for reading planning sources, drafting requirements, saving a PRD and reviewing optional tracker publication](/diagrams/to-prd/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/to-prd/sequence.svg)\n\n</details>\n\n## Preserve the source and the corrections\n\nThe PRD records where its decisions came from. Later explicit corrections take precedence over earlier choices, while other material conflicts need to be surfaced.\n\nIf I changed the retry scope during the conversation, I want that correction reflected in the document. If two source documents disagree and neither clearly supersedes the other, I want to see the conflict rather than have the AI choose silently.\n\nThe source decision record stays intact. By default, the PRD is saved as a separate Markdown file in an existing planning or PRD directory. If there is no suitable directory, the skill uses `docs/prds/`.\n\nFor this example, that could be:\n\n```text\ndocs/prds/2026-08-06-export-retry-prd.md\n```\n\nAn explicit revision updates the intended PRD while preserving its stable IDs and unrelated edits. The final response identifies the saved file, its readiness and the remaining decisions.\n\n## Publishing is a separate step\n\nThe default output is local. If I want the PRD published to a tracker, I ask for that separately.\n\nThe skill then resolves the destination, checks the fields and any duplicate concerns, and presents the concrete proposal. I can choose **Publish**, **Edit** or **Cancel**. Cancelling keeps the local document.\n\nA blocked PRD can be published as a clearly identified draft if I explicitly approve that. Publishing it does not make its open decisions disappear.\n\n## The skill and what follows\n\nYou can find the [`write-requirements` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/write-requirements/write-requirements.md) on GitHub.\n\nA ready PRD can move to `plan-implementation`, which breaks the requirements into implementation slices. That is the next part of the workflow, and another adaptation of Matt Pocock's work. It starts when I request it; writing a PRD does not automatically create issues or begin implementation.\n\nFor me, this step is about carrying the planning decisions forward in a form that is useful to build and review against. The AI helps organize the requirements, while I still need to check that they represent what we intended.\n\nHappy coding!\n"},{"slug":"letting-claude-challenge-the-plan","title":"Letting AI challenge the plan","date":"2026-06-11","updated":"2026-09-14","tags":["AI","skills","planning"],"url":"https://www.rasmusolsson.dev/posts/letting-claude-challenge-the-plan/","excerpt":"An idea can sound clear until someone asks what should happen when it fails, who should be allowed to use it, or what we actually need in the first version. Those questions can cha...","content":"\nAn idea can sound clear until someone asks what should happen when it fails, who should be allowed to use it, or what we actually need in the first version. Those questions can change the plan before we have written any code.\n\nI want the AI to help find those gaps. It can read the available context, challenge assumptions and ask about the decisions that still need my judgment.\n\nMy `grill-me-pragmatic` skill was inspired by [Matt Pocock's `grill-me` skill](https://www.aihero.dev/skills-grill-me). The version described here is my adaptation, with an emphasis on keeping the conversation practical and writing down the resulting decisions. Matt's skill has continued to evolve too; this article describes mine as of its updated date.\n\n![A planning sheet with a branching decision tree, one highlighted question and a stack of decision notes](/images/grill-me-pragmatic/cover.webp)\n\n## Why pragmatic?\n\nThere is a balance here. I want the AI to make sensible choices from the context and keep planning efficient, while still involving me in decisions that need my judgment.\n\nSometimes it makes too much explicit: every small choice becomes a question or a lengthy explanation. Other times it leaves too much implicit and moves ahead on an assumption we should have discussed.\n\nThe pragmatic part is an attempt to loosen that up a bit. I give the AI more room to handle ordinary choices without spelling out every detail, and ask it to focus the conversation on decisions that could meaningfully change the plan.\n\nThat does not guarantee it will get the balance right. I still need to notice when it assumes too much or spends too long on something minor. But for me, it has helped make the planning conversation less rigid and reduced some of the unnecessary back-and-forth.\n\n## Start with the outcome\n\nI do not need a finished specification to begin. I can bring an idea, an existing plan or a document with unresolved decisions. This example uses my skill’s Claude Code command:\n\n```text\n/raholsn:grill-me-pragmatic We want users to retry failed report exports from the dashboard. Help clarify the first version using the existing export flow.\n```\n\nThis is an illustrative example. It gives the AI assistant an outcome and a place to look, while leaving the scope open for discussion.\n\nThe skill starts by restating the plan briefly and identifying the decisions that matter. It is a standalone planning conversation; it does not require a tracker or company profile.\n\n## Read first, then ask\n\nIf the code already shows how exports are created, the AI should inspect that flow. Asking me to describe something it can discover adds work and risks replacing evidence with my recollection.\n\nThe exploration stays focused on the current decision. Relevant handlers, tests, configuration or client behavior can establish part of the answer without requiring a complete repository audit.\n\nFor example, imagine the existing export flow creates a separate job for each request and retains failed attempts. That would give us a starting point for discussing retries. It would not settle whether users should be able to retry every failure or only particular ones. That depends on the behavior we want and the constraints we need to respect.\n\nThe AI should summarize what it found and ask about the remaining decision.\n\n## One question at a time\n\nThe skill presents a recommendation before each question. That gives me a concrete proposal to assess and a reason for it.\n\nAn illustrative exchange might look like this:\n\n```text\nRecommended answer: Limit the first version to retrying an export with its original parameters. That keeps retry behavior separate from editing and requesting a different report.\n\nQuestion: Should users be able to change the report parameters when they retry?\n```\n\nI can accept that, reject it or explain a missing requirement. If my answer leaves an important ambiguity, the skill asks a tighter follow-up.\n\nThe ordering matters too. Deciding whether a retry preserves the original request comes before discussing how an editing screen should work. Once a decision is resolved, the skill tracks it and moves to the dependent questions instead of asking me to confirm it again.\n\nRecommendations remain proposals. A confident suggestion does not establish the product requirement or make the tradeoff mine by default.\n\n## Let the AI handle the ordinary choices\n\nNot every branch of the plan needs a question:\n\n| Situation | What the skill should do |\n|---|---|\n| Existing code or documents establish the answer | Read them and explain the finding. |\n| A choice is conventional, low-risk or easy to reverse | Make the choice and state the assumption briefly. |\n| A decision changes scope, user behavior or a consequential commitment | Recommend an answer and ask one focused question. |\n| An answer would not materially affect the plan | Skip the question. |\n\nIn the export example, following the application's established naming convention is usually something the AI can handle. Deciding who may retry another user's export needs more care because it changes permissions and user behavior.\n\nThe skill considers areas such as failure handling, data, rollout and testing when they matter to the plan. Those areas are prompts for judgment, not a requirement to ask a question about every topic.\n\n### The planning workflow\n\nThese diagrams describe the skill instructions, not a recorded planning session.\n\n![Planning workflow showing context exploration, independent choices, focused questions and the saved decision record](/diagrams/grill-me-pragmatic/overview.svg)\n\n[Open the overview at full size](/diagrams/grill-me-pragmatic/overview.svg)\n\n<details>\n<summary>Explore the planning conversation</summary>\n\nThe sequence shows how the skill uses available context, follows up on ambiguous answers and records the outcome.\n\n![Detailed sequence of a planning conversation from the initial idea through decisions, assumptions and the saved record](/diagrams/grill-me-pragmatic/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/grill-me-pragmatic/sequence.svg)\n\n</details>\n\n## Know when to stop\n\nThe conversation finishes when the relevant decisions are concrete enough to move forward and the remaining unknowns are explicit, acceptable assumptions. There will almost always be more detail we could discuss.\n\nSome unknowns also need evidence beyond a conversation. If we do not know whether users understand the proposed retry behavior, more confident wording from the AI will not answer that. The next useful step might be a prototype or feedback from the people who will use it.\n\nI need to keep the conversation directed toward the outcome and be clear when a recommendation relies on something we have not established.\n\n## Keep the decisions after the conversation\n\nThe skill writes a concise Markdown decision record containing:\n\n- The plan summary.\n- Decisions and their reasoning.\n- Assumptions and why they are acceptable.\n- Material open risks or questions.\n- A suggested next step.\n\nIt records the outcome rather than the full conversation. I can choose the output path; otherwise it uses an existing planning directory or creates `docs/planning/`.\n\nFor the illustrative export plan, the default filename could be:\n\n```text\ndocs/planning/2026-07-02-export-retry-grill-decisions.md\n```\n\nThe record gives the next stage something concrete to work from. I can use it directly or ask `write-requirements` to turn it into requirements. Neither happens automatically, and the grilling conversation does not start implementation. I have to explicitly end planning and ask for execution.\n\n## The skill\n\nThanks to Matt Pocock for the inspiration. His [`grill-me` source](https://github.com/mattpocock/skills/tree/main/skills/productivity/grill-me) is available in his skills repository.\n\nYou can find the [`grill-me-pragmatic` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/grill-me-pragmatic/grill-me-pragmatic.md) on GitHub.\n\nFor me, the value is in finding the decisions that deserve attention before they become assumptions in the code. The AI can help expose them, while I stay responsible for the direction and the choices we carry forward.\n\nHappy coding!\n"},{"slug":"making-claude-plugins-easier-to-reuse","title":"Making skills useful across workplaces","date":"2026-05-27","updated":"2026-09-14","tags":["AI","skills","productivity"],"url":"https://www.rasmusolsson.dev/posts/making-claude-plugins-easier-to-reuse/","excerpt":"A useful development workflow often contains more company knowledge than it first appears to. The tracker team, engineering conventions and the place to look for logs can all end u...","content":"\nA useful development workflow often contains more company knowledge than it first appears to. The tracker team, engineering conventions and the place to look for logs can all end up embedded in its instructions.\n\nThat works until I want to use the workflow somewhere else. Reviewing a pull request is still reviewing a pull request, but the repository, tools and engineering conventions have changed.\n\nMany skills need workplace context to be useful. If we want to use them across different companies, we need an easy way to configure them with the tools, conventions and knowledge relevant to each workplace.\n\nI use both Codex and Claude Code, so I want to think about these workflows in terms of the skills and context they need.\n\nThat is where my `profile` skill comes in. It gives the other skills a shared configuration so we can adapt them to the workplace without rewriting each one.\n\n![A portable toolbox of workflow cards beside two folders with different configuration cards](/images/portable-plugins/cover.webp)\n\n## The skill needs the right context\n\nThe same skill can be useful at different companies, provided it has the context to do the job well. Its instructions can describe how to approach the task, while configuration and supporting guidance supply what changes between workplaces.\n\nFor example, a review workflow can say “read the relevant testing guidance and assess the changed behavior.” The company's expectations for regression coverage and the evidence required before delivery belong in the context supplied to that workflow.\n\nThe same applies to operational tools. An investigation can have a consistent method while each company supplies its own targets, access and field mappings.\n\nThe profile design is still evolving. This post explains the separation I am aiming for, using the current implementation as an example, rather than presenting its configuration format as a settled interface. It describes the workflow as of its updated date.\n\n## Give each kind of information a place\n\nI find it useful to separate four things:\n\n| Part | What belongs there |\n|---|---|\n| Reusable workflow | How to understand a task, investigate evidence, review findings and report results |\n| Company profile | Shared settings such as tracker selection, repository roots and connection names |\n| Optional company pack | Confirmed engineering conventions, testing guidance, domain knowledge and runbooks |\n| Repository guidance | Instructions specific to the application, its structure and how it is built and tested |\n\nThe pack is a directory of supporting material referenced by the profile. It supplies company knowledge that several skills can use without repeating it in each skill's instructions.\n\nThis also gives a rule a clear owner. A company's testing policy should come from that company. The workflow can ask for it and apply it, but it should not invent one when the document is missing.\n\nThe shared convention and testing references are deliberately empty templates. If no company document is supplied, a skill can use repository guidance and observed patterns. It should still distinguish “this is how the existing tests are written” from “this is a confirmed company requirement.”\n\n## One workflow, different workplaces\n\nWhen we are already working in a repository, the AI can usually inspect the project and work out what “build” means. We do not need to make every discoverable detail a setting just to make a skill reusable.\n\nThe more useful configuration is the context the repository cannot reliably tell us. Which tracker team should receive a new ticket? Where are the company's engineering guidelines? Which logging connection and environment are relevant to this investigation?\n\nConsider creating a ticket at two different companies. Both might use Linear, but the team, project and conventions for writing a useful ticket can differ. The skill can keep the same workflow for understanding the request, preparing a draft and reviewing it with me. The profile supplies the relevant tracker settings, and company guidance supplies the local expectations.\n\nThat is the kind of reuse I want: configure the workplace context once and let the skills use it, without maintaining a separate version of every workflow for each company.\n\n## Make setup use the evidence already available\n\nEasy configuration should involve fewer questions about things the repository already tells us.\n\nThe current `profile` skill inspects repository evidence before proposing settings. It can use remotes and existing guidance to establish context, then ask about the workplace choices that remain unclear.\n\nI still need to resolve choices that the evidence cannot establish: the intended repository when remotes are ambiguous, the tracker team, or whether to use a company pack.\n\nThe current implementation uses these Claude Code commands. Loading or initial setup starts with:\n\n```text\n/raholsn:profile\n```\n\nAnd inspecting the effective settings:\n\n```text\n/raholsn:profile show\n```\n\n`show` reads and reports without writing or starting setup. That makes it useful for understanding what a workflow will use before asking it to do work.\n\n### The current configuration workflow\n\nThese diagrams come from the profile guide. They describe the current instructions for loading, showing and replacing configuration; they are not a recorded setup run.\n\n![Profile workflow showing read-only inspection, existing-profile validation, and setup with preservation on failure](/diagrams/portable-plugins/overview.svg)\n\n[Open the overview at full size](/diagrams/portable-plugins/overview.svg)\n\n<details>\n<summary>Explore configuration loading and setup</summary>\n\nThe sequence includes evidence gathering, developer choices and how an existing file is preserved during replacement.\n\n![Detailed profile sequence covering repository evidence, configuration resolution, setup decisions and backup before replacement](/diagrams/portable-plugins/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/portable-plugins/sequence.svg)\n\n</details>\n\n## Make the effective settings visible\n\nMultiple sources of configuration need a clear order. The current implementation resolves them from shared defaults, through the matching repository override and explicit repository guidance, to explicit instructions for the current invocation.\n\nFor example, I can ask an implementation workflow to keep its changes local for this task even when the usual delivery settings describe draft PRs. That changes the scope of this invocation without rewriting the company profile.\n\nThe resolved summary should show which repository and sources were used, along with anything unset. Each new workflow invocation resolves the profile afresh and shares that snapshot with its downstream steps. Otherwise, a setting left over from another repository could quietly affect the next task.\n\nThat visibility matters more to me than hiding every configuration detail. I want setup to be convenient and the resulting behavior to be understandable.\n\n## Optional should mean optional\n\nA portable workflow should work with the capabilities that are available, while being clear about what it could not do.\n\nNo tracker can be fine for general development: work from the description and skip tracker actions. A request to create a ticket needs a tracker connection. Those are different situations.\n\nA missing company pack does not prevent the workflow from using repository guidance. It does mean the workflow should not claim to have checked company rules it was never given. A malformed or inaccessible dependency also needs an explanation; it should not disappear into the same category as an optional integration that was never configured.\n\nThis keeps a minimal setup useful without making it look more complete than it is.\n\n## The tool still matters\n\nThe examples show one implementation. Reusing a workflow in another AI tool may require adapting how its skills, plugins and connections are configured.\n\nA profile can name an MCP connection, but it does not install or authenticate that connection. A company pack can describe a system, but that does not grant access to it. Secrets belong in the appropriate connection or credential setup, and the resolved summary should not print them.\n\nThere are limits to portability too. A workflow that uses a particular provider's API or CLI still needs support for that provider. Changing a host name in JSON cannot turn a GitHub-specific helper into an implementation for another platform.\n\nI want the configuration to expose those dependencies honestly. Reuse comes from separating the parts that vary and defining how they are consumed, not from assuming every tool is interchangeable.\n\n## The profile skill\n\nYou can find the [`profile` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/profile/profile.md) on GitHub.\n\nThe implementation includes example profiles and starter documents for company conventions and testing guidance. The exact shape may change as the design develops.\n\nThe goal is to keep improving the same skills and give them the context they need at each workplace. The profile skill provides a shared way to configure that context, while the local decisions stay with the people who own them.\n\nHappy coding!\n"},{"slug":"asking-database-questions-with-claude","title":"Asking database questions in natural language","date":"2026-04-23","updated":"2026-09-14","tags":["AI","skills","SQL"],"url":"https://www.rasmusolsson.dev/posts/asking-database-questions-with-claude/","excerpt":"Sometimes the next step in an investigation is to look at the database. Has a record changed status? Are there other records in the same state? How many were created during the per...","content":"\nSometimes the next step in an investigation is to look at the database. Has a record changed status? Are there other records in the same state? How many were created during the period we are investigating?\n\nWriting the SQL is only part of that work. I also need to understand the schema, choose the right environment and make sure the query answers the question without doing unnecessary work on the database.\n\nMy `sql-server-reader` skill gives the AI assistant a workflow for that. It uses the available repository and database context to prepare a read, explains what it expects the query to do, and lets me review it before execution.\n\n![A database beside a table of records, with a magnifying glass highlighting one row and a query card ready for review](/images/sql-server-reader/cover.webp)\n\n## Start with a question\n\nI can describe what I need without writing SQL first. The examples here use the skill’s Claude Code command:\n\n```text\n/raholsn:sql-server-reader Look up the current status of export job <id> in the staging reporting database\n```\n\nOr start with a broader question:\n\n```text\n/raholsn:sql-server-reader Count export jobs created in the staging reporting database between April 22, 2026 00:00 UTC and April 23, 2026 00:00 UTC, grouped by current status\n```\n\nThese are illustrative examples. The identifier, target and available entities depend on the configuration. I can also supply a SELECT statement for the AI to review.\n\nThe distinction between those questions matters. A lookup needs the relevant record. A count needs an aggregate over the intended population. Returning the first twenty rows and counting them would not answer the second question.\n\nBefore preparing the read, the AI checks the context it has: repository queries, schema definitions, entity descriptions and company guidance. If the target or meaning of a field remains unclear, it asks.\n\n## Two ways to reach the data\n\nThe skill supports two Microsoft tools. The configured connection determines which path it uses.\n\n| | Microsoft SQL MCP Server | Microsoft sqlcmd |\n|---|---|---|\n| Connection | MCP access to selected entities | Direct connection to a configured server and database |\n| Prepared read | Structured arguments supported by the exposed tools | A bounded SELECT statement and client invocation |\n| Context | Entity descriptions and available fields | Repository evidence and approved metadata queries |\n\n[Microsoft SQL MCP Server](https://learn.microsoft.com/en-us/azure/data-api-builder/mcp/overview) is built on Data API builder. It exposes configured entities through tools and applies the configured permissions. The reader skill discovers the actual read operations available; it does not treat that connection as an endpoint for arbitrary SQL.\n\nFor environments with direct database access, [sqlcmd](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility?view=sql-server-ver17) runs the prepared SQL. The skill checks the installed variant before choosing authentication and connection options.\n\nThe company supplies the connection, read permissions and relevant database context. Adding a target name to a profile does not establish access. If MCP cannot express a query or cannot reach an entity, the skill reports the limitation. Switching to direct SQL needs a configured target and explicit authorization for that scope.\n\n## Review the read before running it\n\nThis is the central interaction. The AI presents the exact SQL or structured MCP arguments, the target and environment, the limits, and a short assessment of the likely database work. Then I choose:\n\n| Choice | What happens |\n|---|---|\n| **Run** | Execute the displayed read once its access and performance requirements are satisfied. |\n| **Cancel** | Stop the proposed read. |\n| **Edit** | Revise the proposal, reassess it and show the choices again. |\n\nThat review also applies when I supplied the SQL myself. The initial request starts preparation; the displayed proposal is what I approve. After I choose Run, the skill does not ask for a second confirmation for that same operation.\n\nPermission checks, catalog queries and estimated-plan queries need their own approval too. Local files and non-query MCP descriptions can be inspected during preparation. A changed query, target or budget, or a retry after failure, needs a new reviewed proposal.\n\nFor MCP, I can approve a bounded read that includes pagination, provided the preview states the total budget. That does not authorize unrelated follow-up queries.\n\n### Workflow diagram\n\nThe diagrams describe the skill instructions, not a recorded database investigation.\n\n![SQL reader workflow from choosing the connection and preparing a bounded read through cost review and Run, Cancel or Edit](/diagrams/sql-server-reader/overview.svg)\n\n[Open the overview at full size](/diagrams/sql-server-reader/overview.svg)\n\n<details>\n<summary>Explore the two read paths</summary>\n\nThe sequence shows preparation, the execution decision and how the MCP and sqlcmd paths return their evidence.\n\n![Detailed SQL reader sequence showing the developer review and the MCP and sqlcmd read paths](/diagrams/sql-server-reader/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/sql-server-reader/sequence.svg)\n\n</details>\n\n## A small result can still mean a lot of work\n\nA query returning twenty rows might still need to sort a large dataset. A count returns one value, but calculating it can involve many records. Row limits and timeouts are useful limits; they do not establish that a query is inexpensive.\n\nThe skill reviews filters, joins, ordering and aggregation against the available index, size and plan evidence. A migration can show that an index was intended. It does not prove the index exists in the target database or that the query will use it.\n\nFor the export-job example, the created-time filter needs to match the stored timestamp semantics. A suitable range may let the database use an index more effectively than applying a function to each stored timestamp. Any rewrite still has to preserve the requested time window and answer.\n\nIf the likely cost remains a material concern, the AI should explain it and resolve the scope before running. Quietly shortening a month to a day would change the question. An existing reporting target or summary view might be appropriate, but that choice needs to be visible.\n\nI want this assessment to help me review the read. It is not a promise of a particular execution plan or runtime.\n\n## Explain what the result proves\n\nSuppose the illustrative lookup returns an export job in a failed state. That establishes the returned state, subject to the connection's freshness limits. It does not explain why the job failed. The next step might be to follow its identifier through the logs.\n\nThe count example has a similar distinction: jobs created during a time window, grouped by their current status, does not tell me how many changed to failed during that window. Those questions need different evidence.\n\nA useful answer should include:\n\n- The target, filters and fixed time bounds used.\n- Whether the result is a lookup, sample or complete aggregate.\n- Any remaining pages, truncated values or missing fields.\n- Known freshness limits, including caching on the MCP path.\n- The conclusion supported by the data and what remains unknown.\n\nSeparate pages are not automatically a consistent snapshot of changing data. With sqlcmd, successful execution does not by itself prove that formatted output captured every value. Those limits matter when drawing a conclusion.\n\n## Read access is part of the setup\n\nThe skill has no write mode. It does not modify records, create indexes, execute stored procedures or change permissions during an investigation. The underlying tools support more than reading, so the configured permissions need to match the intended access. Instructions alone are not database access control.\n\nResults stay in the conversation by default. Exports require an explicit request, and the skill does not create an automatic local audit log. It can report available query or audit identifiers without claiming that every read has been durably recorded.\n\n## The skill\n\nYou can find the [`sql-server-reader` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/sql-server-reader/sql-server-reader.md) on GitHub.\n\nThe full instructions will live there, including connection setup and query-review rules. This article describes the workflow as of its updated date.\n\nFor me, the useful part is being able to ask a database question where I am already working, then review how the AI proposes to answer it. I still own the decision to run the read and the conclusions I draw from the result.\n\nHappy coding!\n"},{"slug":"following-the-evidence-through-logs-with-claude","title":"Let AI do the log digging","date":"2026-03-19","updated":"2026-09-14","tags":["AI","skills","observability"],"url":"https://www.rasmusolsson.dev/posts/following-the-evidence-through-logs-with-claude/","excerpt":"Logs contain a lot of useful information, but connecting it takes work. Whether investigating an alert, checking unexpected behavior during development or following up on a reporte...","content":"\nLogs contain a lot of useful information, but connecting it takes work. Whether investigating an alert, checking unexpected behavior during development or following up on a reported issue, understanding what happened can mean tracing events across several services.\n\nThat digging can be time-consuming and demanding. Timestamps, retries and different identifiers all need to line up before individual log entries tell a coherent story.\n\nAI can help with that work: finding relevant records, following identifiers and bringing related events into a timeline. I want it to highlight what deserves attention and show the supporting evidence, so I can assess the explanation and decide what to investigate next.\n\nMy `logs` skill gives that work a structure. It searches the configured telemetry store, follows request relationships where available and presents an answer with its search scope and limitations.\n\n![Connected log records arranged around a request timeline, with one event under a magnifying glass](/images/logs/cover.webp)\n\n## Start with the question\n\nA support ticket might say:\n\n> The user tried to export their report around 09:05 UTC, but it never became available. User ID: `<user-id>`. The error screen showed trace ID `<trace-id>`.\n\nThis is an illustrative ticket. It contains both the symptom and a starting point for the search. I can give that context to the assistant along with the environment and time window. This example uses the skill’s Claude Code command:\n\n```text\n/raholsn:logs Investigate this support ticket in staging: the user's report export never became available around 09:05 UTC on March 19, 2026. Trace ID: <trace-id>. User ID: <user-id>. Search 09:00–09:15 UTC and correlate the relevant events across services.\n```\n\nThe identifiers are placeholders. If the ticket only has a user ID, the assistant can use it to find candidate requests within the time window, where that field is logged, then follow their trace or correlation IDs. One user may have several unrelated requests, so matching the user ID alone does not establish that every event belongs to the reported issue.\n\nThe command also supports questions that do not start with a particular request:\n\n```text\n/raholsn:logs Find the slowest export-service requests in staging during the last hour\n```\n\nThose are different investigations. The first needs the sequence of events for one request. The second needs duration data across matching operations, including ones that completed successfully. Filtering everything to errors would miss part of the question.\n\n## Connect the skill to the right telemetry\n\nThe current implementation uses Elastic Agent Builder through MCP. Elastic provides the connection and tools; the skill supplies the investigation method. The profile identifies the streams or indices to search, the field mappings and any local logging notes.\n\nElastic's [Agent Builder and MCP reference architecture](https://www.elastic.co/search-labs/blog/agent-builder-mcp-reference-architecture-elasticsearch) explains how its tools can be exposed to an MCP client. The deployment needs to support that connection and grant access to the relevant data. Adding an index name to a profile does not establish access by itself.\n\nThe skill checks the available tools and actual field mappings before querying. Different logging setups can represent the same concept differently, so it should not assume a field name or severity scale from a familiar-looking record.\n\nThis command uses read-only query and discovery tools. Investigating an incident does not authorize it to restart a service or change application data.\n\n## The workflow\n\nThe investigation follows five steps:\n\n1. **Establish the scope.** Resolve the environment, telemetry target, time window and filters. Ask when the intended target is unclear.\n2. **Check access and fields.** Verify the connection and how the required data is represented.\n3. **Query for the question.** Choose a trace lookup, error search, latency query, aggregate or ordinary event search.\n4. **Follow the relevant evidence.** Inspect request relationships and associated logs when they help answer the question. Check for incomplete results.\n5. **Present the answer.** Show the supporting records or aggregates, explain the interpretation and identify the remaining limits.\n\n### Workflow diagram\n\n![Logs workflow from scope and access checks through queries, coverage checks and an evidence-based answer](/diagrams/logs/overview.svg)\n\n[Open the overview at full size](/diagrams/logs/overview.svg)\n\n<details>\n<summary>Explore the detailed workflow</summary>\n\nThe sequence shows how the query changes with the question and when the investigation follows a trace or refines its search.\n\n![Detailed sequence for querying logs, following traces and reporting coverage](/diagrams/logs/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/logs/sequence.svg)\n\n</details>\n\n## The last error might be a symptom\n\nReturning to the illustrative support ticket, suppose the API logs a timeout after calling a worker, and the worker has recorded a slow storage operation under the same trace.\n\nA useful answer should establish the sequence before naming a cause. Did the storage operation start before the API timed out? Was the worker still processing afterward? Do the recorded durations account for the delay, or is there an unexplained gap?\n\nThe AI can use the available spans to outline the request path, then inspect the logs around the operation that needs attention. Instead of having to hold the whole sequence in my head while moving between searches, I can review the connected events, their timing and the records behind them. If span relationships are missing, it can still build a timeline from the matching records, but it should say that the call structure is incomplete.\n\nSuppose the records show that the storage call occupied most of the time before the API timeout. That supports investigating the storage call. It does not, by itself, prove whether the underlying cause was throttling, networking or something in the application.\n\nI want the answer to preserve that distinction. The evidence can tell us where to look next without settling the root cause yet.\n\n## Keep the search window consistent\n\n“During the last hour” changes as time passes. The skill resolves that window once into fixed UTC bounds so its follow-up queries refer to the same period.\n\nSometimes a request continues outside the initial window, especially with asynchronous work. Following it further can be useful, but that wider search should be identified separately. It should not silently change a count I requested for the original period.\n\nThe selected indices matter just as much. No matches in the wrong environment tell us very little about the incident. A failed query is also different from a successful query with no results.\n\n## Counting errors is a different task\n\nI can also ask for an aggregate:\n\n```text\n/raholsn:logs Count export-service error events per minute in staging during the last hour\n```\n\nFor that question, the skill should aggregate the matching events before limiting the rows returned to the conversation. Fetching a small sample and counting its errors would answer a different question.\n\nIt should also explain what was counted. One failed request might produce several error events across services, so an event count is not necessarily a count of failed requests.\n\nWhen a tool truncates a response, the investigation needs to narrow or split its queries and check coverage. If completeness cannot be established, that remains part of the result.\n\n## What I want back\n\nThe answer should contain enough evidence for me to inspect the reasoning:\n\n- The environment, stream and exact time window searched.\n- The relevant records, identifiers and timing, or the requested aggregates.\n- What those records support and what remains an interpretation.\n- Missing data, truncation or access limitations that affect the conclusion.\n\nI do not need every payload pasted into the conversation. I need the fields that matter and a clear path back to the relevant events.\n\nThe default result is an answer in the conversation. Unlike the implementation and review workflows, this skill does not require a saved Markdown report.\n\n## The skill\n\nYou can find the [`logs` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/logs/logs.md) on GitHub.\n\nThe full instructions will live there, including the query and field-mapping rules. This article describes the workflow as of its updated date.\n\nFor me, the useful part is having help connecting the records while keeping the evidence visible. The AI can guide the investigation, but I still need to assess whether the explanation holds and what action the situation calls for.\n\nHappy coding!\n"},{"slug":"giving-claude-a-workflow-for-implementation","title":"Giving AI a workflow for coding tasks","date":"2026-02-12","updated":"2026-09-14","tags":["AI","skills","productivity"],"url":"https://www.rasmusolsson.dev/posts/giving-claude-a-workflow-for-implementation/","excerpt":"Asking an AI assistant to implement a task leaves quite a few things implied. Which requirements should it follow? How should it break up the change? What needs testing, and what s...","content":"\nAsking an AI assistant to implement a task leaves quite a few things implied. Which requirements should it follow? How should it break up the change? What needs testing, and what should happen when a review finds a problem?\n\nI want those expectations written down. My `work` skill gives the AI assistant a process for taking one task through implementation, review and validation, with a record of the decisions and results along the way.\n\nThe developer still owns the task and the decision to accept the change. The workflow makes the work easier to follow and inspect; it does not turn generated code into something we can approve without reviewing it.\n\n![An implementation workflow with code, tests and review cards connected by a return loop](/images/work/cover.webp)\n\n## Start with one task and a clear scope\n\nThe skill accepts a ticket or an actionable description. These examples use its Claude Code command:\n\n```text\n/raholsn:work TEAM-123\n```\n\n`TEAM-123` is an example ticket. With tracker access configured, the skill reads its requirements. A task can also be supplied directly without creating a ticket.\n\nThe normal workflow includes implementation commits, pushing the task branch and creating or updating its PR. That is a meaningful amount of delegation, so I want the scope to be clear at the start. For local work, I can narrow it explicitly:\n\n```text\n/raholsn:work Make the CSV export respect the selected filters. Keep the changes local, do not commit or push.\n```\n\nThe command should follow that instruction through the whole session. It does not automatically take on a backlog, merge the result, deploy it or message a colleague.\n\n## Check the starting point\n\nBefore implementing anything, the workflow resolves the repository settings, records the task and checks the prerequisites. A preflight agent reads the requirements and checks the relevant tools, sign-in, local services, tracker access and Git state.\n\nThose checks answer specific questions. A reachable service port proves connectivity, not that the application is healthy. A successful tracker connection does not establish that the task is clear enough to implement.\n\nExisting changes also matter. The workflow inspects them before preparing the implementation checkout, using an isolated working copy when needed. It should not stash or discard unrelated work simply to make its own task easier.\n\nOnce the starting point is established, the AI reads the relevant code and writes an implementation brief. Consequential gaps in the requirements need a decision before the affected work begins.\n\n## Give the supporting agents a purpose\n\nThe main agent coordinates the task. Other agents have narrower responsibilities:\n\n| Agent | Responsibility |\n|---|---|\n| Preflight | Establish whether the task has the prerequisites it needs. |\n| Cross-repository explorer, when needed | Answer a specific question about a related repository. |\n| Architect and applicable domain reviewers | Assess the proposed design against the system and supplied requirements. |\n| Planner | Divide delivery into coherent steps with checks for each one. |\n| Functional reviewer | Examine the implemented behavior and possible regressions. |\n| Retrospective | Record useful lessons from the actual session. |\n\nThe value is in the different responsibilities and the evidence they return. Their findings still need to be checked. A concern should change the implementation because it is supported by the code and requirements, not simply because another agent raised it.\n\n## Plan changes that can be reviewed\n\nLet's use the CSV export as an illustrative example. The task says the exported data should respect the filters selected by the user.\n\nBefore writing code, the workflow needs to establish which filters are supported and whether the export covers all matching rows or just the visible page. That is product behavior. It should not quietly pick an answer because one implementation is easier.\n\nOnce the behavior is agreed, a plan might separate applying the filter criteria from connecting them to the export request, with appropriate tests for each change. The exact steps depend on the repository; the point is that each step has a clear purpose and a way to check it.\n\n### Prepare and plan\n\nThe first panel from the skill guide shows the readiness checks, task understanding and delivery planning. Blue boxes represent work assigned to supporting agents.\n\n![Work steps zero to four: readiness checks, task understanding, design review and planning](/diagrams/work/prepare.svg)\n\n[Open the preparation diagram at full size](/diagrams/work/prepare.svg)\n\n## Implement, validate, review, repeat\n\nFor each planned step, the AI loads the relevant context, implements the change and runs the configured checks. A reviewer then examines the saved diff. Supported findings lead back to a correction and another validation pass.\n\nThere are two loops here: improving the current step until its checks are satisfied, and moving through the remaining steps in the plan.\n\nA failed test is a reason to investigate. An unavailable test environment is a limitation to report, not a passing result. If a required check cannot run, the workflow needs an explicit decision about that limitation before proceeding with delivery.\n\nIn the normal commit-based workflow, a completed step records its commit and validation evidence. With an explicit no-commit request, the result remains local and the commit steps stay pending.\n\n### Implementation and review loop\n\n![Work steps five to nine: implement, validate, review, fix and record the step](/diagrams/work/implement.svg)\n\n[Open the implementation diagram at full size](/diagrams/work/implement.svg)\n\n## Check the complete task\n\nPassing each step's checks is useful, but the pieces still need to work together. Before delivery, the workflow maps the selected acceptance criteria to the implementation and validation evidence, and reviews the cumulative change where that has not already been covered.\n\nFor the export example, tests around filter construction do not establish that the export endpoint actually uses those filters. The whole-task check needs evidence for the connected behavior. A gap here sends the task back to implementation.\n\nWhen publication is in scope, the workflow pushes the validated commits and creates or updates the PR. It then reads the actual PR diff, revisions and observed CI results. An empty local diff after committing is not evidence that the delivered change has been reviewed.\n\nThe handoff should say what was delivered, what was checked and what remains unresolved. A local test pass and a successful push do not mean that CI passed or that the PR is ready to merge.\n\n### Verification and delivery\n\n![Work steps ten to fourteen: whole-task verification, delivery checks and handoff](/diagrams/work/deliver.svg)\n\n[Open the delivery diagram at full size](/diagrams/work/deliver.svg)\n\nThe guide includes optional comment handling and post-merge actions. Those require the relevant authorization; the command does not merge the PR or wait indefinitely for someone else to do so.\n\n<details>\n<summary>Explore the complete sequence</summary>\n\nThis diagram shows the interactions between the developer, coordinator, supporting agents, repository and PR host. It describes the full supported workflow, including optional paths.\n\n![Complete Work sequence from task selection through implementation and delivery](/diagrams/work/sequence.svg)\n\n[Open the complete sequence at full size](/diagrams/work/sequence.svg)\n\n</details>\n\n## Leave enough context to resume\n\nLonger tasks get interrupted. The skill keeps an external session folder with the original task, implementation brief, delivery plan, review findings and validation results.\n\nWhen resuming, it checks that the session still matches the repository, branch, commits and requirements. It can continue from the first incomplete phase without treating old findings as evidence for a different revision.\n\nThis is also useful for me as the developer. I want to inspect what changed, understand why a decision was made and see which checks were actually run.\n\n## The skill\n\nYou can find the [`work` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/work/work.md) on GitHub.\n\nThe full instructions will live there, including the supporting references and session files. This article describes the workflow as of its updated date.\n\nFor me, the useful part is making the implementation process explicit. The AI can carry out the agreed steps and keep the evidence together. I remain responsible for the requirements, the scope of that delegation and reviewing the resulting change before accepting it.\n\nHappy coding!\n"},{"slug":"working-through-pr-feedback-with-claude","title":"Working through PR feedback with AI","date":"2026-01-08","updated":"2026-09-14","tags":["AI","skills","productivity"],"url":"https://www.rasmusolsson.dev/posts/working-through-pr-feedback-with-claude/","excerpt":"A pull request has come back with review comments. Some point to bugs, some suggest a different approach, and others need a conversation before anything should change. Working thro...","content":"\nA pull request has come back with review comments. Some point to bugs, some suggest a different approach, and others need a conversation before anything should change. Working through them means understanding the feedback and deciding what to do with it.\n\nIn [AI-assisted code reviews](/posts/a-second-look-at-your-pull-request-with-claude), I described how the AI can help structure a review while the developer remains responsible for the findings. The same applies on the receiving end. A suggestion deserves investigation, whether it came from a colleague or an AI reviewer.\n\nMy `fix-comments` skill gives that follow-up a structure: assess the feedback, let me decide on the actions, and keep track of what actually reaches the PR.\n\n![Review comments being assessed and checked alongside a revised code sheet](/images/fix-comments/cover.webp)\n\n## Understand the feedback before changing the code\n\nIt is tempting to start with the first comment and work down the page. But two comments might describe the same underlying problem, or a proposed fix might conflict with another requirement.\n\nI want to see the whole assessment before the code starts changing. For each comment, the AI checks the current source, the relevant requirements and the discussion around it. It records what supports the concern, what remains unclear, and what a correction would involve.\n\nThis gives me something concrete to review. I can challenge the assessment, ask for more evidence, or take an item myself. The goal is to make a considered change and explain it to the reviewer.\n\n## Starting the workflow\n\nWith repository access configured, the Claude Code command for this implementation is:\n\n```text\n/raholsn:fix-comments example/export-service#42\n```\n\nThe repository and PR number are examples. The default mode is interactive. The AI investigates the selected review items and presents the complete assessment before editing code, posting replies or resolving threads.\n\nIt reads the conversations, including replies and general PR comments. An outdated line reference does not automatically mean the concern has been fixed; the current code still needs checking.\n\n## Assess, decide, then apply\n\nThe workflow has five main stages:\n\n1. **Capture the feedback.** Save the selected comments and their original replies, with an index that identifies the PR revision.\n2. **Assess every item.** Check each claim against the source and requirements, then record the evidence and proposed action.\n3. **Present the whole assessment.** Include valid concerns, uncertain items and comments that may already be addressed. Pause for my decisions.\n4. **Apply the selected fixes.** Make the agreed changes and validate the combined result. Account for fixes that address more than one comment.\n5. **Confirm the outcome.** Deliver authorized changes to the PR, then reply and resolve eligible threads. Report anything still pending.\n\nAt the decision step, I have three choices:\n\n| Choice | What it means |\n|---|---|\n| Fix | Apply the supported proposed correction and carry it through validation and authorized delivery. |\n| I will review manually | Leave the code and PR comment untouched; keep it as my follow-up. |\n| Ignore | Take no action on the item in this pass and record that decision. |\n\nIgnoring an item does not resolve the thread or tell the reviewer that their concern was invalid. Unanswered items also remain pending.\n\n### Workflow diagram\n\n![Assessment, developer decisions, fixes and delivery in the fix-comments workflow](/diagrams/fix-comments/overview.svg)\n\n[Open the overview at full size](/diagrams/fix-comments/overview.svg)\n\n<details>\n<summary>Explore the detailed workflow</summary>\n\nThe diagram includes both supported modes. This post focuses on the default interactive path, which pauses after the complete assessment for developer decisions.\n\n![Detailed sequence for assessing and addressing PR review comments](/diagrams/fix-comments/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/fix-comments/sequence.svg)\n\n</details>\n\n## Three comments do not necessarily mean three fixes\n\nLet's continue with a background CSV export as an illustrative example. Suppose a PR has these comments:\n\n- A retry might send the user a second download notification.\n- The notification code needs a check that the export has not already been completed.\n- Export files should be retained for a different length of time.\n\nThe first two may describe the same defect. The AI should trace the behavior, check whether protection already exists, and propose one correction if the concern is supported. The validation should exercise the retry and check how many notifications are sent.\n\nThe retention comment is a different decision. If the requirements do not establish the retention period, the AI should flag that missing context. Choosing a new period would change the product behavior, so I need to resolve that with the relevant people.\n\nI might choose Fix for the first two items and I will review manually for the third. The resulting record should show that one fix addressed two comments, while the retention question is still open.\n\n## Keep a record of the decisions\n\nThe skill saves a `task-feedback` folder with the original comments, an index, a combined assessment and a file for each item. The reviewer's words stay separate from the assistant's analysis.\n\nEach item records the chosen action, changes, validation and outcome. That distinction is useful when work stops halfway through. A fix can be written locally but still need testing or delivery. A question can remain open even though the other comments are done.\n\nThe record should make those differences clear when I return to the PR.\n\n## A local edit is not a delivered fix\n\nAfter applying the selected changes, the workflow validates the combined diff and prepares the intended commits. In interactive mode, pushing requires approval unless it was already authorized. If the PR has advanced, the new changes need to be incorporated and checked before delivery.\n\nOnly after a code fix has reached the PR does the command reply to the original comment with the fix commit and validation result. Thread resolution follows when supported and appropriate. If validation or delivery is blocked, the item stays open.\n\nChoosing Fix authorizes the described correction and follow-up; it is not a separate prompt before every reply. I can ask to review reply wording before it is posted when I want that additional checkpoint. I remain responsible for the changes and communication made on my behalf.\n\n## The automatic option\n\nThere is also an explicit `auto_fix=true` mode. It still presents the assessment, but continues with clear, safe actions without waiting for individual choices. It can apply fixes, push them and finalize eligible replies and resolutions. Uncertain items are deferred.\n\nThat is a broader delegation than the workflow I have described here. For the interactive approach, I leave it at the default so I can assess the proposed actions first. A finding that needs a product decision should remain a question in either mode.\n\n## The skill\n\nYou can find the [`fix-comments` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/fix-comments/fix-comments.md) on GitHub.\n\nThe full instructions will live there, including how comments are selected and how partial outcomes are handled. This article describes the workflow as of its updated date.\n\nFor me, the useful part is the separation between understanding the feedback and acting on it. The AI helps investigate and track the work, while I decide which changes are justified and verify the result.\n\nHappy coding!\n"},{"slug":"a-second-look-at-your-pull-request-with-claude","title":"AI-assisted code reviews","date":"2025-12-03","updated":"2026-09-14","tags":["AI","skills","productivity"],"url":"https://www.rasmusolsson.dev/posts/a-second-look-at-your-pull-request-with-claude/","excerpt":"AI can be useful in code reviews, but it is easy to end up at either extreme: dismissing it entirely or accepting its findings without checking them. I think there is a practical m...","content":"\nAI can be useful in code reviews, but it is easy to end up at either extreme: dismissing it entirely or accepting its findings without checking them. I think there is a practical middle ground.\n\nThe developer is still responsible for reviewing the code and deciding whether to approve the change. The AI can support that work by giving the review a structure, gathering relevant context and examining the change from different perspectives.\n\nI want it alongside me during the review, surfacing findings that I can investigate. Before a finding becomes a PR comment, I check the code, assess the impact and decide whether the feedback is useful enough to post. In this post I will go through how my `code-review` skill supports that approach.\n\n![A code diff examined through user-flow, system-design and test-coverage notes](/images/code-review/cover.webp)\n\n## What I want from an AI review\n\nThere is quite a bit to keep in mind when reviewing someone else's change. What was requested? Can the user complete the flow? What happens if one of the calls fails? Do the tests actually check the behavior we care about?\n\nI want the skill to help cover those perspectives and give me concrete things to investigate. A comment such as “consider improving error handling” does not give me much to work with. A finding that explains which failure leaves the user stuck, and points to the code responsible, is much more useful.\n\nThe motivation is a structured review that helps me examine the change thoroughly, including its behavior, failure scenarios, security and test coverage. Several AI reviewers can share the same blind spot, so their agreement alone does not establish that something is wrong or correct.\n\n## Start with the pull request\n\nWith repository access configured, this example uses my raholsn skill through its Claude Code command:\n\n```text\n/raholsn:code-review example/export-service#42 post_comments=none\n```\n\nThe repository and PR number are examples. `post_comments=none` keeps the findings in the review output, so I can read them before deciding what to do with them.\n\nThe skill reads the PR description, changes and existing comments. When it finds a linked ticket and has access to the tracker, it also reads the requirements and acceptance criteria. Those requirements are shared with every selected reviewer.\n\nThis matters because the code tells us what was implemented. It does not necessarily tell us what should have been implemented. If the ticket cannot be read, the review should say that instead of claiming the acceptance criteria have been checked.\n\n## Different reviewers, different questions\n\nThe main command coordinates separate agents with focused responsibilities. Functional review always runs. Architecture and QA join when the change makes them relevant, or when explicitly requested. Configured domain or compliance reviewers run when their scope applies.\n\n| Reviewer | Main question |\n|---|---|\n| Functional | Does the change deliver the requested behavior through the affected user journey? |\n| Architecture | Does the design hold up under the relevant failure, concurrency and load scenarios? |\n| QA | What evidence shows that the behavior works, including the important edge cases? |\n| Domain or compliance | Does the change follow the supplied rules that apply to this area? |\n\nFor a small documentation change, bringing in every specialist would add little value. A change to retries, data handling or service boundaries deserves a broader look.\n\nThe company-specific rules come from configuration and reference documents. The command should not invent a policy because the repository happens to contain something that looks like payment code.\n\n## The workflow\n\nThe review goes through these steps:\n\n1. **Gather the context.** Read the PR, available requirements and repository guidance. Record the exact revision being reviewed.\n2. **Choose the reviewers.** Select the perspectives relevant to the change and explain any skips.\n3. **Review in parallel.** Give each selected agent the same revision and requirements, then let it write its findings independently.\n4. **Combine the results.** Check conflicting claims against the source, remove duplicates and prioritize by demonstrated impact.\n5. **Present the assessment.** Show concerns, supporting evidence, open questions and limitations. Post comments only according to the selected preference or a subsequent user choice.\n\nThe source revision is an easy detail to overlook. My local checkout might be on another branch, or someone might push a new commit while the review is running. The reviewers need the source that belongs to the captured PR revision. Before posting comments, the command checks whether that comparison has changed and refreshes affected findings if necessary.\n\n### Workflow diagram\n\n![Code review workflow overview](/diagrams/code-review/overview.svg)\n\n[Open the overview at full size](/diagrams/code-review/overview.svg)\n\n<details>\n<summary>Explore the detailed workflow</summary>\n\nThe sequence below comes from the skill guide and includes its supported branches. For the developer-led approach in this post, use the local review and individually selected comments path.\n\n![Detailed code review sequence](/diagrams/code-review/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/code-review/sequence.svg)\n\n</details>\n\n## One change, three perspectives\n\nLet's say a PR adds retries to a background CSV export. This is an illustrative example, not a finding from an actual review.\n\nThe ticket says the user should receive one download link when the export is ready. A storage request times out, and the worker tries again.\n\n**The architecture reviewer** follows what happens around the timeout. Could the first upload have succeeded even though the response was lost? Does the retry create another export? Is there an existing key or state check that prevents duplicate work?\n\n**The functional reviewer** follows the outcome for the user. Does the export remain stuck in “processing”? Could the user receive two notifications? Does the download still contain the rows they requested?\n\n**The QA reviewer** checks the evidence. Is there a test where the upload succeeds but the response is lost, followed by a retry? Does it assert the final state and number of notifications, or only that no exception was thrown?\n\nThese questions overlap, which is useful. The consolidation step should turn the overlap into a clear finding rather than three comments about the same issue. If the code already handles the scenario, the concern should be dropped. If the expected behavior is unclear, that remains a product question.\n\n## Keeping the findings useful\n\nA finding should identify the affected code, explain the scenario and describe its impact. The command groups results into critical issues, warnings, suggestions and areas checked without concerns.\n\nMissing tests alone should not turn into a critical defect. There needs to be a distinction between a demonstrated problem, a meaningful validation gap and an optional improvement.\n\nIt also matters what the review could not establish. Reading a test is different from running it. A reviewer that only had access to the diff may be missing behavior elsewhere in the repository. Those limits belong in the assessment.\n\n## Posting comments\n\nFor this approach, I keep the initial review local with `post_comments=none`. I work through the findings, check the supporting code and decide which comments I want to put my name behind. A severity label from the AI does not make that decision for me.\n\nWhen I want the AI to post feedback, I explicitly identify the findings I have reviewed and approve their wording. With no posting preference supplied, the skill also offers an individually selected findings option after presenting its assessment. It supports broader posting options, but the approach described here keeps a developer decision before each comment is posted.\n\nThe review itself does not apply fixes or merge the PR. It gives me an assessment and, when requested, puts the relevant feedback where the author can act on it.\n\n## The skill\n\nYou can find the [`code-review` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/code-review/code-review.md) on GitHub.\n\nThe full instructions will live there, including how the command passes context to the reviewers and combines their output. This article describes the workflow as of its updated date.\n\nI think the useful part of this approach is combining a structured review with developer judgment. The AI helps me explore the change and raises questions worth checking. I remain responsible for the feedback I post and the decision to approve the PR.\n\nHappy coding!\n"},{"slug":"creating-linear-tickets-with-a-claude-skill","title":"Let AI handle the ticket admin","date":"2025-10-28","updated":"2026-09-14","tags":["AI","skills","productivity"],"url":"https://www.rasmusolsson.dev/posts/creating-linear-tickets-with-a-claude-skill/","excerpt":"Writing tickets is probably not the most exciting part of being a developer. You already know what needs to change, but you still need to open the tracker, write the description, a...","content":"\nWriting tickets is probably not the most exciting part of being a developer. You already know what needs to change, but you still need to open the tracker, write the description, and select the right project and milestone. With Linear connected to my AI assistant, I can describe the work where I’m already working and review the ticket before it’s created.\n\nI want to describe the work while I have the context in front of me, and let a skill take care of preparing the ticket. In this post I will go through `create-linear-ticket`, a skill for preparing and reviewing a ticket before creating it.\n\n![Rough notes becoming a structured ticket, with a conversation in between](/images/create-linear-ticket/cover.webp)\n\n## Why a skill for creating tickets?\n\nThe useful part is having my usual workflow written down. Each developer has their own Linear user and works with different projects and milestones. Those choices should come from their configuration, without having to repeat them in every request.\n\nThere is also the description itself. A quick note can make sense when you write it, but leave the next person wondering what actually needs to change. The skill turns that note into a draft, keeps unanswered questions visible, and gives me a chance to review it before anything is created.\n\nA skill here is a set of instructions for an AI assistant to follow. The Linear connection provides the tools to read and create issues; the skill describes how to use those tools for this task. It needs a configured connection and the relevant access before it can create anything.\n\n## Starting with a description\n\nThis example uses the Claude Code command from my raholsn plugin, with Linear configured:\n\n```text\n/raholsn:create-linear-ticket The CSV export does not include the filters selected on the page. It should export the same rows that the user is looking at.\n```\n\nThat gives it the observed problem and the intended behavior. It does not tell it why the export is wrong, because we have not investigated that yet.\n\nAn illustrative draft could look like this:\n\n> **Apply selected filters to the CSV export**\n>\n> The CSV export does not include the filters selected on the page. The expected behavior is that the exported rows match the selected filters.\n>\n> **Proposed acceptance criteria**\n>\n> - Selected filters apply to the exported rows.\n> - Exporting without filters continues to work.\n>\n> **Open question**\n>\n> - Should the export include all matching rows or only the current page?\n\nThat last question matters. “The rows the user is looking at” could mean two different things when pagination is involved. I would rather clarify that than have the ticket quietly decide it for us.\n\n## The workflow\n\nThe command goes through five steps:\n\n1. **Read my configuration.** Find the Linear connection and my default team, assignee and status.\n2. **Prepare the ticket.** Resolve the project and milestone, draft the description, and check the requested fields against Linear.\n3. **Check for duplicates.** Search the team for similar issues and show any plausible matches.\n4. **Let me review.** Show the title, description and final fields, then wait for my choice.\n5. **Create and verify.** Create the approved ticket and return its link and confirmed fields.\n\nAt the review step I have three choices:\n\n| Choice | What happens |\n|---|---|\n| Create | Create the ticket with the details shown. |\n| Edit | Change the draft or fields, then review the revised version. |\n| Cancel | Stop without creating a ticket. |\n\nIf it finds a possible duplicate, it asks whether a separate ticket is needed. It also tells me when the search could not be completed. An empty search result is useful information, but it is not a guarantee that nobody has reported the same thing before.\n\n### Workflow diagram\n\n![Create linear ticket workflow overview](/diagrams/create-linear-ticket/overview.svg)\n\n[Open the overview at full size](/diagrams/create-linear-ticket/overview.svg)\n\n<details>\n<summary>Explore the detailed workflow</summary>\n\nThe sequence below comes from the skill guide and includes its supported branches. The default path pauses for Create, Edit or Cancel; the separate auto path requires an explicit opt-in.\n\n![Detailed create linear ticket sequence](/diagrams/create-linear-ticket/sequence.svg)\n\n[Open the detailed diagram at full size](/diagrams/create-linear-ticket/sequence.svg)\n\n</details>\n\n## Remembering my projects and milestones\n\nDefaults cover the values I normally use. A preset groups choices for a particular kind of work, such as a project and its current milestone.\n\nFor example, if I configure a preset called `reporting`, I can select it explicitly:\n\n```text\n/raholsn:create-linear-ticket Apply selected filters to the CSV export --reporting\n```\n\n`reporting` is an example preset, not something built into the skill. Each developer configures the values that make sense for them.\n\nThe order is straightforward: an explicit instruction overrides the selected preset, and the preset overrides my defaults. I can use my usual project while assigning one particular ticket to someone else.\n\nAn assignee default of `me` means the user authenticated through the Linear connection. It does not switch between accounts.\n\n## When I want to skip the review step\n\nThe default is to show the draft and wait. There is also an explicit `--auto` flag for a request that already contains enough information:\n\n```text\n/raholsn:create-linear-ticket Document the CSV export file format --reporting --auto\n```\n\nThis still runs the field validation and duplicate checks. If it needs a decision, it stops and returns the draft with the reason. It should not guess a project or ignore a possible duplicate just to finish the request.\n\nThe same applies when a write times out. The ticket might already exist, so the skill checks what happened before reporting the result. It does not blindly create another one.\n\n## The skill\n\nYou can find the [`create-linear-ticket` skill](https://github.com/raholsn/raholsn-claude-plugin/blob/main/claude-plugin/skills/create-linear-ticket/create-linear-ticket.md) on GitHub.\n\nThe full instructions will live there so they can be read and adapted without copying the entire skill into this post. This article describes the workflow as of its updated date.\n\nFor me, this is a useful size for an AI workflow: one ticket, a few repeatable checks, and a clear point where I can review the result. I still need to decide whether the ticket describes the right work.\n\nHappy coding!\n"},{"slug":"azure-sql-resource-governance-microservice-architecture","title":"Azure SQL resource governance microservice architecture","date":"2025-07-17","tags":["devops"],"url":"https://www.rasmusolsson.dev/posts/azure-sql-resource-governance-microservice-architecture/","excerpt":"In a microservice environment you can end up with a lot of databases, sometimes because you want hard ownership boundaries, sometimes because it was the easiest way to avoid coupli...","content":"\nIn a microservice environment you can end up with a lot of databases, sometimes because you want hard ownership boundaries, sometimes because it was the easiest way to avoid coupling. Either way, the question that tends to show up is not \"should we share compute\", it's:\n\n> **What controls do we actually have to keep cost and performance predictable when we have many databases?**\n\nA useful starting point is to think in terms of **how compute is allocated**. In practice, most setups lean in one of these two directions:\n\n- **Dedicated compute per database or per service:** great isolation and a simple mental model, but it can get expensive when many databases are idle most of the time. A \"just in case\" dedicated allocation that sits quiet for large parts of the day is still something you pay for continuously. A useful variant here is **serverless**, which can be great for a single database with truly intermittent usage. It can scale within a min and max range and optionally pause when idle. **Serverless tends to cost more per vCore-hour than provisioned**, so it usually only wins when average utilization is low enough that scaling down (and possibly auto-pause) compensates for the higher unit price. You still get predictable bounds by sum of min and max per database, but spend lives within a range, and if you enable auto-pause you also accept some resume and warm-up latency.\n\n- **Shared compute across many databases:** one underlying compute budget shared by multiple databases, which tends to fit the microservice \"long tail\" where a few databases are hot and many are small, spiky, or mostly idle. This is a popular choice  cost-wise, because you pay for compute once and let databases share it.\n\nThis is also where shared compute starts to feel risky in practice. Not because sharing is wrong, but because **it only takes one database to change the behavior of the whole environment**.\n\nA common example is a new service that looks harmless on paper, but then ships with a heavy query, a migration that runs during peak, or a background job that suddenly does a full-table scan. On a shared compute setup, that workload does not just hurt itself, it competes for the same CPU, memory, workers and I/O as everything else. The symptom you see is rarely \"service X is slow\", it's more often \"a bunch of unrelated services got slower at the same time\".\n\nThe uncomfortable part is that the cost-efficient model (\"share compute\") also makes the failure mode broader (\"share pain\") even though limiting blast radius is often seen as one of the benefits of a microservice architecture.\n\nOn Azure SQL (SQL Server), we have a surprisingly useful toolbox for handling shared compute without completely giving up guardrails:\n\n- **Resource governance** Azure SQL Server Database enforces limits via a resource governance layer that is based on SQL Server Resource Governor, adapted for the cloud.\n- **Azure SQL Server Elastic Pools**, which let many databases share a pool of compute while still giving you controls like per-database **min/max DTU**.\n- The option to move specific databases out of the pool when they stop behaving like \"small and spiky\" workloads.\n\nThis is worth calling out because it is not equally available across engines or cloud providers. A lot of the \"elastic pool\" story is tightly connected to SQL Server. Azure SQL Database's ability to enforce limits is built on a governance layer that is based on SQL Server Resource Governor concepts, adapted for the cloud. That is why Azure can offer pooled compute plus per-database caps as a first-class product feature.\n\n## Basic examples of limiting blast radius with min/max (DTU / vCore)\n\nThe most practical guardrail in an elastic pool is the **per-database min/max** setting. It caps how much compute a database can use when active, and it can reserve baseline capacity if you set a minimum above zero.\n\nImportant detail: **the configured min and max apply to all databases in the pool.** If you need different caps or guarantees for different services, the usual approach is to **split workloads into separate pools** (or move an outlier to dedicated compute).\n\nBelow are a few common patterns.\n\n### Example 1: Isolate a “risky” workload by putting it in its own pool\n\nIf you have a service that runs imports, backfills, or experimental queries, consider placing it in a separate elastic pool with a smaller budget and tighter per-database limits.\n\n**Why this helps:** its worst day cannot consume the same shared budget as the rest of your estate.\n\n**Shape:**\n\n- Long-tail pool: tuned for lots of small, spiky databases\n- Risky/batch pool: smaller pool budget, lower max-per-db\n\n### Example 2: Protect critical workloads with a dedicated budget boundary\n\nIf one database is truly on the critical path, the most reliable protection is to give it its own pool (or dedicated compute). This makes both performance and spend easier to reason about.\n\n**Why this helps:** you avoid critical latency being dependent on what else happens to be active in a shared pool.\n\n**Shape:**\n\n- Critical pool: sized for the SLO of the critical workload\n- Long-tail pool: everything else\n\n### Example 3: Long-tail defaults plus explicit outlier handling\n\nA common operational setup is to keep one predictable pool for the long tail, then handle exceptions explicitly through placement.\n\n**Shape:**\n\n- Pool A (Long tail): most microservice databases, low average usage\n- Pool B (Hot): known high-traffic databases\n- Pool C (Batch/maintenance): jobs, migrations, backfills (optional)\n\n**Why this helps:** it keeps the default simple, makes outliers obvious, and makes promotions a normal operational decision.\n\n### Example 4: A database outgrows its current pool\n\nA service can start small and fit nicely in the long-tail pool, then evolve over time.\n\n**Signals:**\n\n- It frequently hits the per-database max in its pool\n- Latency starts correlating with other activity in the pool\n- New requirements appear (for example compliance, isolation, stricter SLOs)\n\n**Options:**\n\n- Increase the pool size if the pool is simply lacking headroom\n- Move the database to a different pool that matches the workload shape\n- Move it to dedicated compute if you want tighter isolation and independent scaling\n\nThe main idea is that pools are great for the long tail, but it is normal to adjust placement as requirements evolve.\n\n\n## Further reading\n\nIf you want to go deeper into the details, here are the official docs I tend to reference:\n\n- Azure SQL Database elastic pool overview: <https://learn.microsoft.com/en-us/azure/azure-sql/database/elastic-pool-overview?view=azuresql>\n- Manage elastic pools (Portal, CLI, PowerShell, T-SQL): <https://learn.microsoft.com/en-us/azure/azure-sql/database/elastic-pool-manage?view=azuresql>\n- Azure SQL Database resource management and governance (how limits are enforced): <https://learn.microsoft.com/en-us/azure/azure-sql/database/resource-limits-logical-server?view=azuresql>\n- SQL Server Resource Governor (the feature the governance model is based on): <https://learn.microsoft.com/en-us/sql/relational-databases/resource-governor/resource-governor?view=sql-server-ver17>\n\nHappy coding!\n"},{"slug":"3-workflows-to-diagnose-a-kubernetes-pods","title":"3 workflows to diagnose a Kubernetes Pod","date":"2025-05-07","tags":["kubernetes","devops"],"url":"https://www.rasmusolsson.dev/posts/3-workflows-to-diagnose-a-kubernetes-pods/","excerpt":"When something goes wrong in production (memory growth, CPU spikes, stuck threads, networking issues), you often need diagnostics from a running Kubernetes Pod. In this post I will...","content":"\nWhen something goes wrong in production (memory growth, CPU spikes, stuck threads, networking issues), you often need diagnostics from a running Kubernetes Pod.\n\nIn this post I will go through three workflows I see most teams end up with:\n\n1. **Use the tools already inside the application container**\n2. **Inject an ephemeral debug container** (break-glass toolbox)\n3. **Use a planned diagnostics setup** (sidecar or diagnostics profile)\n\nI’ll keep this post Kubernetes-first and use .NET memory dumps as the example, but the same three approaches apply to other runtimes and tools too (Java, Node.js, Go, Python, native apps).\n\n## Why diagnostics is tricky in Kubernetes\n\nMany production images are intentionally minimal:\n\n- no shell\n- no package manager\n- no debugging utilities\n\nThis is good for security and size, but it changes how you debug. The “VM approach” (SSH in and install tools) usually doesn’t apply.\n\n## Option 1: Use tools already inside the application container\n\nThis is the simplest workflow and also the fastest when it works.\n\nTypical flow:\n\n1. `kubectl exec` into the running container\n2. Run the diagnostic tool (dump, profile, trace)\n3. Copy artifacts out\n\nExample (using .NET as the diagnostic tooling):\n\n```bash\nkubectl exec -n <ns> -it <pod> -- sh\nps aux\ndotnet-dump collect --process-id 1 --output /tmp/app.dmp\n```\n\n### When option 1 is realistic\n\n- Your image is not distroless and has a shell\n- The diagnostic tools are already included, or your environment allows installing them\n- You can write artifacts somewhere (for example `/tmp` or a mounted volume)\n\n### Common reasons it fails in production\n\n- Minimal images (no shell/tools)\n- Locked-down egress (you can’t download tooling at runtime)\n- Read-only filesystem\n- Copying files out may be harder than expected (some methods require tooling such as `tar`)\n\nOption 1 is great if you already planned for it, but many orgs intentionally avoid shipping tooling inside app containers.\n\n## Option 2: Inject an ephemeral debug container (break-glass toolbox)\n\nEphemeral containers let you attach a temporary container to an *already running pod*.\nThis is a good fit when:\n\n- your app container is minimal\n- you need tools now\n- you want to avoid rebuilding images or restarting the pod\n\nConceptually, you add a toolbox container that has the tools you need (shell, process tools, runtime tooling).\n\nExample injection:\n\n```bash\nkubectl debug -n <ns> -it pod/<pod> \\\n  --image=busybox:1.36 \\\n  --target=<app-container> -- sh\n```\n\n### Important note about installing tools at runtime\n\nA common instinct is to inject a container (for example the .NET SDK image) and then run `dotnet tool install -g dotnet-dump`.\n\nIn my experience this will often **not** work in production, because outbound egress to NuGet is usually blocked, and incident workflows should not depend on live downloads.\n\nInstead, the usual approach is to publish your own small **diagnostics toolbox image** to your container registry (with the tools already installed), and use that image when you inject the ephemeral container.\n\n### Recommended approach: inject a prebuilt diagnostics toolbox image\n\nExample injection (your own image in your registry):\n\n```bash\nkubectl debug -n <ns> -it pod/<pod> \\\n  --image=<your-registry>/dotnet-diagnostics:8.0 \\\n  --target=<app-container> -- sh\n```\n\nInside the debug container you can then collect a diagnostic artifact:\n\n```bash\nps aux\ndotnet-dump collect --process-id 1 --output /tmp/app.dmp\n```\n\nThen copy it out:\n\n```bash\nkubectl cp -n <ns> <pod>:/tmp/app.dmp ./app.dmp\n```\n\n### Fallback: use an SDK image (only if your cluster allows downloads)\n\nIf your cluster allows outbound egress, you can use an SDK image and install tools at runtime:\n\n```bash\nkubectl debug -n <ns> -it pod/<pod> \\\n  --image=mcr.microsoft.com/dotnet/sdk:8.0 \\\n  --target=<app-container> -- bash\n```\n\n```bash\ndotnet tool install -g dotnet-dump\nexport PATH=\"$PATH:/root/.dotnet/tools\"\ndotnet-dump collect --process-id 1 --output /tmp/app.dmp\n```\n\n### Pros\n\n- Works even if the application image is minimal\n- No rebuild required\n- Very flexible for incident response\n\n### Cons\n\n- Requires permissions (RBAC) to create ephemeral containers\n- Some orgs treat it as a security-sensitive “break-glass” action\n- The debug container uses resources (usually small, but it matters if the pod is already near limits)\n- If the workload is OOM-killing fast, you might not have enough time for a full dump\n\nOption 2 is often the most practical approach when it is allowed, because it adapts well to minimal images.\n\n## Option 3: Planned diagnostics (self-service, auditable, automatable)\n\nOption 2 is great when you have cluster permissions and you need to act fast, but it is still a manual workflow. Someone needs to inject a container, run tools, and move artifacts around.\n\nOption 3 is different. The goal is to make diagnostics a **standard product capability** of the platform:\n\n- Repeatable workflow (same steps every time)\n- Controlled access (who can trigger, rate limits)\n- Auditable trail (who triggered what, when, why)\n- Safe artifact handling (storage, retention, encryption)\n- **Self-service for developers**, ideally without asking SRE to jump in\n\nInstead of relying on `kubectl debug` rights, you build a controlled diagnostics path into the platform.\n\n### Building blocks\n\n1. **A diagnostics mechanism inside the workload**\n   - Example: a sidecar like `dotnet-monitor` (or an equivalent tool for your runtime)\n   - Alternative: a small “diagnostics container” that can collect artifacts from the app process\n\n2. **A safe place for artifacts**\n   - A shared volume in the pod (for temporary storage)\n   - Upload to controlled storage (S3/GCS/Azure Blob) with encryption and retention rules\n\n3. **A trigger interface**\n   - A Kubernetes Job that performs the collection\n   - A small internal \"diagnostics controller\" service\n   - A developer-friendly entry point (CLI or Slack command)\n\n### Pattern A: Diagnostics profile enabled via GitOps\n\nThis is a good fit for strict orgs because the diagnostics capability is enabled through a PR, giving you a clean audit trail.\n\n- Normal deployment has no diagnostics sidecar\n- A diagnostics overlay exists (Helm values or Kustomize overlay)\n- Developers (or on-call devs) can open a PR that enables diagnostics for a specific service and environment\n- After the dump is collected, diagnostics is disabled again via PR\n\nExample (Helm values style):\n\n```yaml\n# values-diagnostics.yaml\ndiagnostics:\n  enabled: true\n  # example only\n  tool: dotnet-monitor\n  sharedVolume:\n    type: emptyDir\n    sizeLimit: 5Gi\n  upload:\n    enabled: true\n    destination: blob://prod-diagnostics/<service>/\n    retentionDays: 7\n```\n\nExecution flow:\n\n1. Developer opens PR: \"Enable diagnostics profile for payments-api in prod\"\n2. ArgoCD sync rolls pods with diagnostics enabled\n3. Developer triggers a dump using a standard command (see Pattern C below)\n4. Dump is uploaded, developer receives a link/artifact id\n5. Developer opens PR to disable diagnostics again\n\n### Pattern B: Always-on sidecar, locked down\n\nIf you need faster response, you can keep the diagnostics container always present, but make triggering and access strict:\n\n- NetworkPolicy blocks all access except a specific namespace or identity\n- RBAC limits who can port-forward or run the trigger job\n- Rate limits prevent repeated dump collection\n- Artifacts are uploaded to controlled storage and deleted from the node quickly\n\n### Pattern C: Developer self-service trigger (the important part)\n\nTo make option 3 truly self-service, you want developers to have a single supported way to request diagnostics **without needing elevated kubectl access**.\n\nOne approach that works well in stricter organisations is to let a CLI trigger a **GitHub Actions workflow**. That workflow can require an approval (for example via GitHub Environments) before it is allowed to run in `prod`.\n\nThe workflow then executes the diagnostics on behalf of the developer, using a controlled identity.\n\nIn example:\n\n- Developer runs a CLI command\n- CLI triggers a GitHub Action with inputs (service, env, dump type, incident id)\n- Someone approves (only for sensitive environments like `prod`)\n- The workflow creates a Kubernetes Job that:\n  - talks to the diagnostics container (or runs collection logic)\n  - stores the dump on a shared volume\n  - compresses and uploads it to controlled storage\n  - prints an artifact id or link in the logs\n\nExample:\n\n```bash\n# Developer triggers a dump though CLI\ndiag dump payments-api --env prod --type full --incident INC-1234\n\n# Under the hood the CLI triggers a GitHub Actions workflow, for example:\n# - workflow: \"Diagnostics - collect dump\"\n# - inputs: service=payments-api, env=prod, type=full, incident=INC-1234\n# - requires approval for prod\n\n# After approval, the GitHub Action runs and creates a job in the cluster\n# (example)\nkubectl -n diagnostics create job diag-dump-payments-api \\\n  --image=<your-registry>/diag-runner:1.0\n```\n\nWhat the developer gets back:\n\n- The GitHub Action output (or job logs) contains something like:\n  - \"Dump collected\"\n  - \"Uploaded to blob://prod-diagnostics/payments-api/2026-01-07/app.dmp.gz\"\n  - \"Retention: 7 days\"\n  - \"Incident id: INC-1234\"\n\nYou can also expose the same idea via Slack:\n\n- `/diag dump payments-api prod full INC-1234`\n- Bot triggers the GitHub Action (or calls the diagnostics controller)\n- Approval happens in the same place you already use for production changes\n- Bot returns the artifact link when it is done\n\nThe key point is that developers are not doing ad-hoc kubectl debugging. They are using a supported interface that enforces policy and leaves a clean audit trail (who requested, who approved, and what was collected).\n\n### Pros\n\n- Works even in locked-down clusters (no reliance on `kubectl debug`)\n- Good audit trail (request + approval + execution logs)\n- Makes artifact handling (encryption, retention, access logging) a solved platform problem\n- Self-service for developers without giving broad Kubernetes permissions\n\n### Cons\n\n- Requires up-front platform work (storage, RBAC, retention, the trigger mechanism)\n- Needs careful security design (dumps may contain sensitive data)\n- You still want guardrails (rate limits, require incident id in prod, prefer gcdump by default)\n\n## Conclusion\n\nWe've reviewed 3 different workflows to diagnose a Kubernetes pod.\n\nI can see why option 3 is attractive. A structured workflow with approvals, an audit trail, and a predictable place for artifacts is a nice end state. At the same time, it does take some work to build and maintain. In environments where the cluster is less locked down, you’re a small team, and diagnosing a Kubernetes pod is unlikely to be a frequent task, option 1 or 2 might be a better fit, together with a wiki page describing the usual steps to follow.\n\nHappy Coding!\n"},{"slug":"running-github-actions-locally","title":"Running github actions locally","date":"2025-03-14","tags":["productivity","devops"],"url":"https://www.rasmusolsson.dev/posts/running-github-actions-locally/","excerpt":"It’s pretty common for platform engineers and developers to push changes just to test their GitHub workflows. I’ve done it countless times myself, and honestly, it’s a hassle. You...","content":"\nIt’s pretty common for platform engineers and developers to push changes just to test their GitHub workflows. I’ve done it countless times myself, and honestly, it’s a hassle. You make a small tweak, then push to see if CI actually runs it. That feedback loop often takes several minutes.\n\nToday we’re taking a quick look at the act CLI and how it can help us run GitHub Actions locally to speed up our feedback loops.\n\nThe act library is a small CLI tool that lets you run your GitHub Actions workflows locally on your own machine. It works by spinning up a Docker container that emulate the GitHub runner environment.\n\nThe docker image has various sizes, the higher the size the more tools and pre-installed dependencies it includes, making it closer to GitHub’s real runner environment\nIt’s not guaranteed to run everything exactly the same, but it’s usually close enough to avoid lots of unnecessary pushes.\n\nRunning `act` for the first time requires choosing and downloading the image size:\n\n- Large size image: ~ 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images\n\n- Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most actions\n\n- Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions\n\nLet's test it out on a simple workflow, that just runs dotnet tests for a solution:\n\n```yml\nname: Demo .NET Workflow\n\non:\n  workflow_dispatch:\n\njobs:\n  demo-dotnet:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup .NET SDK\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: \"8.0.x\"\n\n      - name: Restore dependencies\n        run: dotnet restore DemoApp.sln\n\n      - name: Build the solution\n        run: dotnet build DemoApp.sln --no-restore --configuration Release\n\n      - name: Run tests\n        run: dotnet test DemoApp.sln --no-build --configuration Release\n\n    ...\n\n```\n\n\n```bash\nact -j demo-dotnet -W .github/workflows/demo-dotnet.yml --pull=false\n```\n\n![output](/images/running-github-actions-locally/image2.png)\n\nAmazing..!\n\nThis is a simple example, but the idea is what matters. Hopefully, it will gain adoption over time, and fewer devs will brute-force their way through CI with commit histories like \"retry\", \"retry 2\", \"now it will work!\" and instead run GitHub Actions locally to catch the obvious issues first.\n\nReferences:\n\n- <https://github.com/nektos/act>\n- <https://hub.docker.com/r/catthehacker/ubuntu>\n\nHappy coding!\n"},{"slug":"fluent-assertions-license-change","title":"Fluent Assertions License Change","date":"2025-01-22","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/fluent-assertions-license-change/","excerpt":"I’ve relied on Fluent Assertions for years under its Apache 2.0 license, so when v8.0 switched to a commercial license, I had to rethink what free alternatives are available. Why F...","content":"\nI’ve relied on Fluent Assertions for years under its Apache-2.0 license, so when v8.0 switched to a commercial license, I had to rethink what free alternatives are available.\n\n## Why Fluent Assertions Changed Licenses\n\nI don't exactly what the maintainers had in mind, but it feels like Fluent Assertions got big enough that doing bug fixes, feature requests, and support in evenings and weekends just wasn’t cutting it. Adding a paid tier buys them real development time so the lib can stay healthy and they don’t burn out.\n\n## Some Alternatives\n\n**Look for official fork**  \n\nIts not uncommon that the community creates a fork out of the last commit of Apache 2.0 version\n\n- Pros: Continue to get maintainable from the point of the current version\n- Cons: Risk of in-popularity, new maintainers, new adoptions\n- A popular alternative: <https://github.com/AwesomeAssertions/AwesomeAssertions>\n\n**Stay on 7.x**\nStay on v7.x (Apache 2.0)\n\n- Pros: no cost and free critical updates for now.  \n- Cons: No new features, manual upkeep for bug-fixes.\n\n**Migrate to an Alternative**  \nSwap in another OSS assertion library\n\n- Pros: Free, continuous updates\n- Cons: Migration effort, subtle behavior differences. Picking a alternative is sometimes not so straightforward but doing a assessment like looking at github stars, contributors, downloads and other companies might give a good enough assessment.\n- Alternatives: Shouldly, NExpect, Snapshooter, or built-in xUnit/NUnit asserts\n\n**Fork Internally**  \n\nFork v7.x into my org’s repo and treat it like an internal package.  \n\n- Pros: Full control over patches and security updates.  \n- Cons: Maintenance burden\n\n**Purchase a License**  \nBuy the new Fluent Assertions commercial tier.  \n\n- Pros: No migration needed. Access to latest features, official support etc.  \n- Cons: Recurring cost per developer\n\n**Negotiate or Contribute**  \nReach out to the maintainers for a “community” exception or offer contributions in exchange for more lenient terms.  \n\n- Pros: Potential zero-cost path, helps shape project direction.  \n- Cons: Custom agreements and time investments.\n\n## What I Chose\n\nFor now I decided to perform **Stay on 7.x** and I'm leaning on adopting a \"official fork\" maybe <https://github.com/AwesomeAssertions/AwesomeAssertions>.\n\nTo make sure I do not update the fluent assertions package by mistake we can pin the version, and applying license scanning check in CI.\n\nI wrote a post about a custom free solution for keeping track of nuget license here: <https://www.rasmusolsson.dev/posts/tracking-nuget-licenses-with-dotnet-project-licenses>\n\nHappy coding!\n"},{"slug":"time-provider-dotnet-8","title":"Deterministic Testing in .NET 8 Using TimeProvider","date":"2024-12-15","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/time-provider-dotnet-8/","excerpt":"Developers often need to control the current time in tests to ensure consistent results. When using DateTime.UtcNow, test outcomes can vary based on execution time, making them unr...","content":"\nDevelopers often need to control the current time in tests to ensure consistent results. When using DateTime.UtcNow, test outcomes can vary based on execution time, making them unreliable and non-deterministic.\n\nIn this post I will show you an example and how we can make the test deterministic by using a custom implementation as well as the new timeprovider from microsoft that was introduce in .net 8.\n\n## Example: A Shipping Service (non-deterministic)\n\n```csharp\npublic interface IShippingService\n{\n    DateOnly GetEstimatedDeliveryDate(bool express);\n}\n\npublic class ShippingOptions\n{\n    public int StandardShippingDays { get; set; } = 3;\n    public int ExpressShippingDays { get; set; } = 1;\n}\n\npublic class ShippingService : IShippingService\n{\n    private readonly ShippingOptions _options = new();\n\n    public DateOnly GetEstimatedDeliveryDate(bool express)\n    {\n        var daysToAdd = express ? _options.ExpressShippingDays : _options.StandardShippingDays;\n\n        // Using the real clock: DateTime.UtcNow\n        var currentDateOnly = DateOnly.FromDateTime(DateTime.UtcNow);\n\n        return AddBusinessDays(currentDateOnly, daysToAdd);\n    }\n\n    private static DateOnly AddBusinessDays(DateOnly startDate, int businessDays)\n    {\n        var result = startDate;\n\n        while (businessDays > 0)\n        {\n            result = result.AddDays(1);\n\n            if (result.DayOfWeek != DayOfWeek.Saturday &&\n                result.DayOfWeek != DayOfWeek.Sunday)\n            {\n                businessDays--;\n            }\n        }\n\n        return result;\n    }\n}\n\n[Test]\npublic void Should_return_correct_estimated_delivery_date_when_not_express_shipment()\n{\n    var shippingService = new ShippingService();\n\n    var actual = shippingService.GetEstimatedDeliveryDate(express: false);\n\n    var expected = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(3));\n\n    expected.Should().Be(actual);\n}\n\n```\n\nAs we can see in the code above, the test will pass on certain days, but when it hits a weekend, it will start failing.\nWe need to make the date deterministic by setting a fixed date when the test runs.\n\n## Using a custom interface as mock\n\nOne simple approach is to use a custom interface that can be mocked. This allows the application to retrieve a fixed date in tests while still using the real-time value in production. Here's an example:\n\n```csharp\npublic interface IClock\n{\n    DateTime UtcNow { get; }\n}\n\npublic class SystemClock : IClock\n{\n    public DateTime UtcNow => DateTime.UtcNow;\n}\n\npublic class ShippingService : IShippingService\n{\n    private readonly ShippingOptions _options = new();\n    private readonly IClock _clock;\n\n    public ShippingService(IClock clock)\n    {\n        _clock = clock;\n    }\n\n    public DateOnly GetEstimatedDeliveryDate(bool express)\n    {\n        var daysToAdd = express ? _options.ExpressShippingDays : _options.StandardShippingDays;\n\n        // Now we grab the current time from our IClock\n        var currentDateOnly = DateOnly.FromDateTime(_clock.UtcNow);\n\n        return AddBusinessDays(currentDateOnly, daysToAdd);\n    }\n\n    private static DateOnly AddBusinessDays(DateOnly startDate, int businessDays)\n    {\n        var result = startDate;\n\n        while (businessDays > 0)\n        {\n            result = result.AddDays(1);\n\n            if (result.DayOfWeek != DayOfWeek.Saturday &&\n                result.DayOfWeek != DayOfWeek.Sunday)\n            {\n                businessDays--;\n            }\n        }\n\n        return result;\n    }\n}\n\n[Test]\npublic void Should_return_correct_estimated_delivery_date_when_not_express_shipment()\n{\n    var clockMock = new Mock<IClock>();\n    clockMock.Setup(x => x.UtcNow).Returns(new DateTime(2024, 11, 04));\n\n    var shippingService = new ShippingService(clockMock.Object);\n\n    var actual = shippingService.GetEstimatedDeliveryDate(express: false);\n\n    var expected = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(3));\n\n    expected.Should().Be(actual);\n}\n```\n\nAs we can see in the code above, the IClock interface has been injected, and the time is then retrieved.\nFor the text, we mock the interface and returns a predictable time, making the test deterministic.\n\n## The .NET 8 `TimeProvider` Approach\n\nStarting in .NET 8, a custom interface and implementation are no longer required. Microsoft now provides the TimeProvider class as a built-in solution.\n\n```csharp\npublic class ShippingService : IShippingService\n{\n    private readonly TimeProvider _timeProvider;\n    private readonly ShippingOptions _options = new();\n\n    public ShippingService(TimeProvider timeProvider)\n    {\n        _timeProvider = timeProvider;\n    }\n\n    public DateOnly GetEstimatedDeliveryDate(bool express)\n    {\n        var daysToAdd = express ? _options.ExpressShippingDays : _options.StandardShippingDays;\n\n        var utcNow = _timeProvider.GetUtcNow();\n        var currentDateOnly = DateOnly.FromDateTime(utcNow.DateTime);\n\n        return AddBusinessDays(currentDateOnly, daysToAdd);\n    }\n\n    private static DateOnly AddBusinessDays(DateOnly startDate, int businessDays)\n    {\n        var result = startDate;\n\n        while (businessDays > 0)\n        {\n            result = result.AddDays(1);\n\n            if (result.DayOfWeek != DayOfWeek.Saturday &&\n                result.DayOfWeek != DayOfWeek.Sunday)\n            {\n                businessDays--;\n            }\n        }\n\n        return result;\n    }\n}\n\n[TestFixture]\npublic class ShippingServiceTests\n{\n    [Test]\n    public void StandardShipping_OrderedOnFriday_ReturnsWednesday()\n    {\n        var fridayMorning = new DateTimeOffset(2025, 9, 5, 10, 0, 0, TimeSpan.Zero);\n        var fakeTimeProvider = new FakeTimeProvider(fridayMorning);\n\n        var shippingService = new ShippingService(fakeTimeProvider);\n\n        var result = shippingService.GetEstimatedDeliveryDate(express: false);\n\n        var expected = new DateOnly(2025, 9, 10);\n        Assert.That(result, Is.EqualTo(expected));\n    }\n}\n```\n\nMicrosoft also provides FakeTimeProvider as part of the Microsoft.Extensions.TimeProvider.Testing NuGet package.\nIt’s essentially a stub implementation of TimeProvider that allows you to control time in your tests, making it easy to set specific dates and times.\n\n## Conclusion\n\nIt's great to see Microsoft providing standardized solutions for common challenges developers face daily.\nAs developers, we encounter many different approaches to solving the same problem.\nWhile this may seem like a small addition, TimeProvider is another step toward more consistent across different .NET projects.\n\nReferences:\n\n- <https://learn.microsoft.com/en-us/dotnet/standard/datetime/timeprovider-overview>\n"},{"slug":"mapping-code-in-dotnet-manual-complex-source-generators-compared","title":"Mapping Code in .NET: Manual, Complex, and Source Generators Compared","date":"2024-10-12","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/mapping-code-in-dotnet-manual-complex-source-generators-compared/","excerpt":"When working with APIs, databases, or different data models, mapping between types is often necessary. It might feel like extra work, but it helps keep things organized and prevent...","content":"\nWhen working with APIs, databases, or different data models, mapping between types is often necessary.\nIt might feel like extra work, but it helps keep things organized and prevents unintended side effects.\n\nIf the same entity is used everywhere—from the database to the API response a small changes on the entity can have big consequences.\nImagine a Transaction entity with fields like Id, UserId, Amount, CreatedAt, InternalNotes, and IsFlaggedForReview.\nIf this entity is returned directly from an API, fields like InternalNotes (used for fraud detection or internal audits) might unintentionally be exposed to the client.\nWhich is not intended for the end user.\n\nHere’s are a few other reasons for mapping code:\n\n- Prevents accidental data leaks – Ensures only the right data is exposed.\n- Keeps concerns separate – DTOs, entities, and API contracts each serve different purposes.\n- Adds a security layer – Explicitly defining what gets mapped avoids exposing internal structures.\n- Protects API stability – Database changes won’t automatically break API contracts.\n- Simplifies transformations – DTOs allow for computed fields, formatting, and validation without affecting database entities.\n- Improves performance – Returns only the necessary fields, reducing payload size.\n\n**So we surely need mapping**. But how should we map and what are the different alternatives and tools?\n\nIn this post we will look at a few alternatives.\n\n## Mapping tools and alternatives\n\nBased on my experience, I have classified the tools and alternatives into 3 different types:\n\n1. Manual Mapping\n2. Complex mappers\n3. Source generator-based mapping\n\n### Manual mapping\n\nThe most simplest solution is of course to just use you own code to do the mapping, here is an example:\n\n```csharp\n// Internal object\npublic class Transaction\n{\n    public int Id { get; set; }\n    public int UserId { get; set; }\n    public decimal Amount { get; set; }\n    public DateTime CreatedAt { get; set; }\n    public string InternalNotes { get; set; }\n    public bool IsFlaggedForReview { get; set; }\n    public UserDetails User { get; set; }\n}\n\n// Internal object\npublic class UserDetails\n{\n    public int Id { get; set; }\n    public string FirstName { get; set; }\n    public string LastName { get; set; }\n}\n\n// Exposed object\npublic record TransactionDto(\n    int Id, \n    decimal Amount, \n    DateTime CreatedAt, \n    string Status, \n    string UserFullName);\n\n// Usage\nvar transaction = new Transaction\n{\n    Id = 1,\n    UserId = 123,\n    Amount = 150.237m,\n    CreatedAt = DateTime.Now,\n    InternalNotes = \"Suspicious transaction\",\n    IsFlaggedForReview = true,\n    User = new UserDetails { Id = 123, FirstName = \"John\", LastName = \"Doe\" }\n};\n\n// Perform the mapping directly\nvar flaggedForReview = transaction.IsFlaggedForReview ? \"Flagged\" : \"Approved\"; // Convert boolean to string\nvar transactionCreatedAtUtc =  transaction.CreatedAt.ToUniversalTime(); // Ensure UTC\nvar transactionAmount = Math.Round(transaction.Amount, 2); // Round decimal value\nvar fullName = $\"{transaction.User.FirstName} {transaction.User.LastName}\"; // Combine first and last name\n\nvar dto = new TransactionDto(\n    transaction.Id,\n    transactionAmount, \n    transactionCreatedAtUtc, \n    flaggedForReview\n);\n```\n\n### Complex mappers\n\nThe idea behind complex mappers is to automate object-to-object mapping, reducing the need to manually write conversion logic.\nInstead of explicitly mapping each field, you define mapping rules, and the mapper dynamically handles the transformation between objects.\n\nOne of the most widely used tools for this is AutoMapper, which allows defining mappings once and then converting objects automatically.\n\n```csharp\n// Internal object\npublic class Transaction\n{\n    public int Id { get; set; }\n    public int UserId { get; set; }\n    public decimal Amount { get; set; }\n    public DateTime CreatedAt { get; set; }\n    public string InternalNotes { get; set; }\n    public bool IsFlaggedForReview { get; set; }\n    public UserDetails User { get; set; }\n}\n// Internal object\npublic class UserDetails\n{\n    public int Id { get; set; }\n    public string FirstName { get; set; }\n    public string LastName { get; set; }\n}\n\n// Expose object\npublic record TransactionDto(\n    int Id, \n    decimal Amount, \n    DateTime CreatedAt, \n    string Status, \n    string UserFullName);\n\n// Config\npublic class TransactionProfile : Profile\n{\n    public TransactionProfile()\n    {\n        CreateMap<Transaction, TransactionDto>()\n            .ForMember(dest => dest.Status, \n                opt => opt.MapFrom(src => src.IsFlaggedForReview ? \"Flagged\" : \"Approved\")) // bool to string\n            .ForMember(dest => dest.UserFullName, \n                opt => opt.MapFrom(src => $\"{src.User.FirstName} {src.User.LastName}\")) // first and last name\n            .ForMember(dest => dest.CreatedAt, \n                opt => opt.MapFrom(src => src.CreatedAt.ToUniversalTime())) // Ensure UTC\n            .ForMember(dest => dest.Amount, \n                opt => opt.MapFrom(src => Math.Round(src.Amount, 2))) // Round decimal value\n            .ForSourceMember(src => src.InternalNotes, \n                opt => opt.DoNotValidate()); // Ignore field\n    }\n}\n\n//Usage\nvar config = new MapperConfiguration(cfg => cfg.AddProfile<TransactionProfile>());\nvar mapper = config.CreateMapper();\n\nvar transaction = new Transaction\n{\n    Id = 1,\n    UserId = 123,\n    Amount = 150.237m,\n    CreatedAt = DateTime.Now,\n    InternalNotes = \"Suspicious transaction\",\n    IsFlaggedForReview = true,\n    User = new UserDetails { Id = 123, FirstName = \"John\", LastName = \"Doe\" }\n};\n\n// Perform the mapping\nvar dto = mapper.Map<TransactionDto>(transaction);\n```\n\n### Source generator-based mapping\n\nSource generator-based mapping provides a way to automatically generate mapping code at compile time.\nUnlike complex mappers like AutoMapper, source generators generate explicit mapping code before runtime.\nOne of the most popular libraries for this approach is Riok.Mapperly.\n\nHere is a example:\n\n```csharp\n// Internal object\npublic class Transaction\n{\n    public int Id { get; set; }\n    public int UserId { get; set; }\n    public decimal Amount { get; set; }\n    public DateTime CreatedAt { get; set; }\n    public string InternalNotes { get; set; }\n    public bool IsFlaggedForReview { get; set; }\n    public UserDetails User { get; set; }\n}\n\n// Internal object\npublic class UserDetails\n{\n    public int Id { get; set; }\n    public string FirstName { get; set; }\n    public string LastName { get; set; }\n}\n\n// Exposed object\npublic record TransactionDto(\n    int Id, \n    decimal Amount, \n    DateTime CreatedAt, \n    string Status, \n    string UserFullName);\n\n// Mapperly configuration\n[Mapper]\npublic static partial class TransactionMapper\n{\n    [MapProperty(nameof(Transaction.IsFlaggedForReview), nameof(TransactionDto.Status))]\n    [MapProperty(nameof(Transaction.User), nameof(TransactionDto.UserFullName))]\n    [MapperIgnoreSource(nameof(Transaction.UserId))]\n    [MapperIgnoreSource(nameof(Transaction.InternalNotes))]\n    public static partial TransactionDto ToDto(Transaction transaction);\n\n    private static string ConvertStatus(bool isFlaggedForReview) => isFlaggedForReview ? \"Flagged\" : \"Approved\";\n    private static DateTime ConvertToUtc(DateTime createdAt) => createdAt.ToUniversalTime();\n    private static decimal RoundAmount(decimal amount) => Math.Round(amount, 2);\n    private static string CombineUserFullName(UserDetails user) => $\"{user.FirstName} {user.LastName}\";\n}\n\n// Usage\nvar transaction = new Transaction\n{\n    Id = 1,\n    UserId = 123,\n    Amount = 150.237m,\n    CreatedAt = DateTime.Now,\n    InternalNotes = \"Suspicious transaction\",\n    IsFlaggedForReview = true,\n    User = new UserDetails { Id = 123, FirstName = \"John\", LastName = \"Doe\" }\n};\n\n// Perform the mapping\nvar dto = TransactionMapper.ToDto(transaction);\n```\n\nAnd the source generation looks like this:\n\n```csharp\n public static partial class TransactionMapper\n    {\n        [global::System.CodeDom.Compiler.GeneratedCode(\"Riok.Mapperly\", \"4.0.0.0\")]\n        public static partial global::TransactionDto ToDto(global::Transaction transaction)\n        {\n            var target = new global::TransactionDto(\n                transaction.Id,\n                RoundAmount(transaction.Amount),\n                ConvertToUtc(transaction.CreatedAt),\n                ConvertStatus(transaction.IsFlaggedForReview),\n                CombineUserFullName(transaction.User)\n            );\n            return target;\n        }\n    }\n```\n\n## Reviewing the Different Mapping Approaches\n\nWhen working with complex mappers like AutoMapper, one challenge is the lack of visibility into how mappings are applied. While AutoMapper provides configuration profiles to define mappings, understanding the transformations often requires familiarity with the library and diving into its configuration. Based on my experience, AutoMapper can become harder to manage as class structures grow more complex. In contrast, both source generation mappers and manual mapping approaches provide clear visibility into the final mapping logic.\n\nThe source generation example required quite a lot of code. When compared with manual mapping, we can see that we essentially repeat ourselves in the source generation approach.\n\nLooking at, for example:\n\n```csharp\n// mapperly\n[Mapper]\npublic static partial class TransactionMapper\n{\n    [MapProperty(nameof(Transaction.IsFlaggedForReview), nameof(TransactionDto.Status))]\n    [MapProperty(nameof(Transaction.User), nameof(TransactionDto.UserFullName))]\n    [MapperIgnoreSource(nameof(Transaction.UserId))]\n    [MapperIgnoreSource(nameof(Transaction.InternalNotes))]\n    public static partial TransactionDto ToDto(Transaction transaction);\n\n    private static string ConvertStatus(bool isFlaggedForReview) => isFlaggedForReview ? \"Flagged\" : \"Approved\";\n    private static DateTime ConvertToUtc(DateTime createdAt) => createdAt.ToUniversalTime();\n    private static decimal RoundAmount(decimal amount) => Math.Round(amount, 2);\n    private static string CombineUserFullName(UserDetails user) => $\"{user.FirstName} {user.LastName}\";\n}\n\n// mapperly source generation\npublic static partial class TransactionMapper\n{\n    [global::System.CodeDom.Compiler.GeneratedCode(\"Riok.Mapperly\", \"4.0.0.0\")]\n    public static partial global::TransactionDto ToDto(global::Transaction transaction)\n    {\n        var target = new global::TransactionDto(\n            transaction.Id,\n            RoundAmount(transaction.Amount),\n            ConvertToUtc(transaction.CreatedAt),\n            ConvertStatus(transaction.IsFlaggedForReview),\n            CombineUserFullName(transaction.User)\n        );\n        return target;\n    }\n}\n\nvar dto = TransactionMapper.ToDto(transaction);\n\n// manual mapper\nvar flaggedForReview = transaction.IsFlaggedForReview ? \"Flagged\" : \"Approved\"; \nvar transactionCreatedAtUtc =  transaction.CreatedAt.ToUniversalTime(); \nvar transactionAmount = Math.Round(transaction.Amount, 2); \nvar fullName = $\"{transaction.User.FirstName} {transaction.User.LastName}\";\n\nvar dto = new TransactionDto(\n    transaction.Id,\n    transactionAmount, \n    transactionCreatedAtUtc, \n    flaggedForReview\n);\n```\n\nOne of the commonly cited advantages of source generation mappers is that they remove the need to manually specify 1-1 property mappings (e.g., Transaction.TransactionId -> TransactionDto.TransactionId). However, in practice, this benefit is often minimal, especially considering that modern IDEs provide tools like \"initialize members,\" \"multi-cursor\"/\"multi-select,\" and code completion, significantly speed up manual mapping. Additionally, source generation mappers still require explicit transformation logic for computed fields, similar to manual mapping.\n\nA distinct advantage of source generation mappers, however, is their ability to provide compiler warnings for unmapped properties. This can help catch missing mappings at compile time, something that manual mapping does not offer out of the box.\n\nHowever, source generation mappers like Mapperly can sometimes produce unexpected results. Consider the following example:\n\n```csharp\n// Internal object\npublic class Transaction\n{\n    public DateTime? Created { get; set; }\n}\n\n// External object\npublic class TransactionDto\n{\n    public DateTime? CreatedDate { get; set; }\n}\n\n// Mapperly\n[Mapper]\npublic static partial class TransactionMapper\n{\n    public static partial TransactionDto ToDto(Transaction transaction);\n}\n\n// Mapperly source generation\npublic static partial class TransactionMapper\n{\n    [global::System.CodeDom.Compiler.GeneratedCode(\"Riok.Mapperly\", \"4.0.0.0\")]\n    public static partial global::TransactionDto ToDto(global::Transaction transaction)\n    {\n        var target = new global::TransactionDto();\n        target.CreatedDate = transaction.Created?.Date;\n        return target;\n    }\n}\n```\n\nAs we can see above, Mapperly implicitly assumes a conversion based on the naming difference (Created vs CreatedDate). This results in CreatedDate being assigned only the date component of Created, losing the full DateTime value that might have been expected. This kind of implicit transformation can introduce subtle bugs if developers assume full DateTime values are mapped by default.\n\n## Conclusion\n\nCode mappers in .NET solve an important problem, but they come with trade-offs. While tools like AutoMapper and Mapperly can be useful in some scenarios, **manual mapping should not be discarded**. It is clear, reduces complexity, and does exactly what you tell it to do.\n\nHappy coding!\n\nReferences:\n\n- [Automapper](https://github.com/AutoMapper/AutoMapper)\n- [Mapperly](https://github.com/riok/mapperly)"},{"slug":"my-favorite-daily-scripts-right-now-p1","title":"My favorite daily productivity scripts right now","date":"2024-09-10","tags":["productivity"],"url":"https://www.rasmusolsson.dev/posts/my-favorite-daily-scripts-right-now-p1/","excerpt":"I have always been fascinated about productivity and how we as developer can make things less manual. Today I will show my currently most used custom productivity scripts. Create B...","content":"\nI have always been fascinated about productivity and how we as developer can make things less manual.\n\nToday I will show my currently most used custom productivity scripts.\n\n## Create Branch and PR in draft mode (github)\n\nThis script automates the creation of a new branch and a draft pull request, saving a min that would otherwise be spent creating the PR manually.\n\nIt takes three parameters:\n\n- TicketNumber (often required to link the branch to a specific ticket)\n- Description (combined with the ticket number to form the branch name and used in the PR description)\n- BaseBranch (optional, could be enhanced to automatically detect the main branch)\n- It uses a PR template if the repository contains one.\n- It relies on the GitHub CLI (gh), which uses its own authentication token instead of a personal access token.\n- It adds an empty commit because GitHub requires at least one commit to create a pull request.\n\n```powershell\nfunction Create-BranchAndPR {\n    param (\n        [Parameter(Mandatory = $true)]\n        [string]$TicketNumber,\n\n        [Parameter(Mandatory = $true)]\n        [string]$Description,\n\n        [string]$BaseBranch = \"master\"\n    )\n\n    try {\n        # Sanitize the description to create a valid branch name\n        $sanitizedDescription = $Description -replace '[^a-zA-Z0-9\\- ]', '-' -replace '[- ]+', '-'\n\n        # Construct the branch name\n        $branchName = \"$TicketNumber-$sanitizedDescription\"\n\n        # Ensure the base branch is up to date\n        git checkout $BaseBranch\n        git pull origin $BaseBranch\n\n        # Create and switch to the new branch\n        git checkout -b $branchName\n\n        # Create an initial empty commit\n        git commit --allow-empty -m \"Initial empty commit for $branchName\"\n\n        # Push the new branch to the remote repository\n        git push -u origin $branchName\n\n        # Prepare the PR title\n        $TicketNumber = $TicketNumber.ToUpper()\n        $prTitle = \"[$TicketNumber] $Description\"\n\n        # Define the default PR body\n        $defaultPrBody = @\"\n# ${prTitle}\n\nANY ADDITIONAL DESCRIPTION\n\"@\n\n        # Check if a custom PR template exists\n        $prTemplatePath = \".github/PULL_REQUEST_TEMPLATE.md\"\n        $customPrBody = if (Test-Path $prTemplatePath) {\n            Get-Content $prTemplatePath -Raw\n        }\n        else {\n            \"\"\n        }\n\n        # Use the custom PR template if it exists and is not empty; otherwise, use the default\n        $prBody = if (![string]::IsNullOrWhiteSpace($customPrBody)) {\n            $customPrBody\n        }\n        else {\n            $defaultPrBody\n        }\n\n        # Create the draft pull request using the specified PR body\n        $prCreateResult = gh pr create `\n            --title \"${prTitle}\" `\n            --body \"$prBody\" `\n            --draft `\n            --head $branchName `\n            --base $BaseBranch `\n            2>&1  # Capture error output\n\n        if ($LASTEXITCODE -ne 0) {\n            Write-Error \"Failed to create pull request: $prCreateResult\"\n            return\n        }\n       \n        Write-Host \"Draft pull request created successfully!\"\n    }\n    catch {\n        Write-Error \"An error occurred: $_\"\n    }\n}\n```\n\n## Get Deploy Status\n\nA quick way to see the deploy status for different repositories in github when using github actions.\n\nInputs\n\n- Repos – comma-separated repo names (e.g., “api,frontend”).\n- Optional switches let you point at another org/user (-Org), pick a different workflow file (-WorkflowName), change the job it looks for (-JobName), and limit how many runs to inspect (-Limit).\n\nProcess (per repo)\n\n- Calls the GitHub CLI to pull the latest n runs of the specified workflow.\n- For each run (newest first) it fetches the jobs, filters for the job whose name contains $JobName (default “Deploy to prod”), and notes its conclusion.\n- As soon as it finds the first successful run, it stops; until then it counts how many runs were incomplete/failed.\n\nOutput\n\n- For each run examined, it prints the deploy job’s status, completion time, committer, and run URL, colour-coding success green and everything else red.\n- At the end it prints a summary showing how many unsuccessful runs occurred before the most recent successful deploy (or all runs checked if none succeeded).\n\n```powershell\nfunction Get-DeployStatus {\n    param(\n        [Parameter(Mandatory = $true)]\n        [string] $Repos,                            # Comma-separated list of repositories\n        [string] $Org        = \"your org\",          # GitHub organisation / owner\n        [string] $WorkflowName = \"cicd\",            # Workflow name\n        [string] $JobName      = \"Deploy to prod\",  # Job name inside workflow which determine the deploy\n        [int]    $Limit        = 10                 # How many workflow runs to inspect\n    )\n\n    $RepoList = $Repos -split ','                   # Turn list into array\n\n    foreach ($Repo in $RepoList) {\n        Write-Host \"Checking repository: $Repo\" -ForegroundColor Cyan\n        Write-Host \"---------------------------------------------------\"\n\n        $workflowRuns = gh run list `\n            --repo \"$Org/$Repo\" `\n            --workflow \"$WorkflowName\" `\n            --limit $Limit `\n            --json \"databaseId,headSha,url,conclusion\" `\n            --jq '.[]' 2>$null\n\n        if (-not $workflowRuns) {\n            Write-Host \"No workflows named '$WorkflowName' in '$Repo'.\" -ForegroundColor Red\n            Write-Host \"---------------------------------------------------\"\n            continue\n        }\n\n        $workflowRuns   = $workflowRuns | ConvertFrom-Json\n        $incompleteRuns = 0\n        $foundSuccess   = $false\n\n        foreach ($run in $workflowRuns) {\n            if ($foundSuccess) { break }\n\n            $jobs = gh api \"/repos/$Org/$Repo/actions/runs/$($run.databaseId)/jobs\" `\n                   --jq \".jobs[] | select(.name | contains(\\\"$JobName\\\"))\" 2>$null |\n                   ConvertFrom-Json\n\n            if ($jobs) {\n                $commit = gh api \"/repos/$Org/$Repo/commits/$($run.headSha)\" `\n                         --jq '{author: .commit.author.name, date: .commit.author.date}' 2>$null |\n                         ConvertFrom-Json\n\n                $status = if ($jobs.conclusion -eq \"success\") { \"Deployed\" } else { \"Not Deployed\" }\n                $color  = if ($jobs.conclusion -eq \"success\") { \"Green\" } else { \"Red\" }\n\n                Write-Host \" Status       : $status\"               -ForegroundColor $color\n                Write-Host \" Completed At : $($jobs.completed_at -replace 'T',' ' -replace 'Z','')\"\n                Write-Host \" Committed By : $($commit.author)\"\n                Write-Host \" URL          : $($run.url)\"\n            } else {\n                Write-Host \" Status       : N/A\"\n                Write-Host \" Completed At : N/A\"\n                Write-Host \" Committed By : N/A\"\n                Write-Host \" URL          : $($run.url)\"\n            }\n\n            Write-Host \"---------------------------------------------------\"\n\n            if ($run.conclusion -ne \"success\") {\n                $incompleteRuns++\n            } else {\n                $foundSuccess = $true\n            }\n        }\n\n        Write-Host \"\"\n        Write-Host \"============== REPOSITORY SUMMARY ==============\" -ForegroundColor Yellow\n        Write-Host \" Repository                       : $Repo\" -ForegroundColor Yellow\n        Write-Host \" Incomplete Runs Until Completed  : $incompleteRuns\" -ForegroundColor Yellow\n        Write-Host \"===============================================\"\n        Write-Host \"\"\n    }\n}\n```\n\n## Show pull request\n\nA simple script that constructs a URL using the current Git branch and then opens that URL in Chrome (or any other browser).\n\n```powershell\n\nfunction Show-PullRequests {\n    # --------------------------------------------------\n    # Configurable Variables\n    # --------------------------------------------------\n    $organization  = \"[Enter Organization]\"       # Used if needed for other org references\n    $githubOrg     = \"[Enter github org]\"         # The GitHub organization or user namespace\n    $chromePath    = \"[Enter browser exe]\"         # For example C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\n\n    # Use the current directory to determine the project (not strictly required in this script)\n    $currentDirectory = Get-Location\n    $directoryName = Split-Path -Leaf $currentDirectory\n\n    # Validate the Chrome path\n    if (-Not (Test-Path $chromePath)) {\n        Write-Error \"Chrome executable not found at path: $chromePath\"\n        exit\n    }\n\n    try {\n        # Determine the Git root directory\n        $gitRoot = (& git rev-parse --show-toplevel).Trim()\n        $repository = Split-Path -Leaf $gitRoot\n        $project = $repository -split '-' | Select-Object -First 1\n\n        # Get the git remote URL\n        $gitRemote = (& git config --get remote.origin.url).Trim()\n        if (-Not $gitRemote) {\n            Write-Error \"Could not retrieve git remote URL. Ensure it is configured correctly.\"\n            exit\n        }\n\n        # Get the current branch name\n        $branchName = (git rev-parse --abbrev-ref HEAD).Trim()\n        if (-Not $branchName -or $branchName -eq \"HEAD\") {\n            Write-Error \"Could not determine the current branch name. Ensure you are on a branch.\"\n            exit\n        }\n\n        # Construct the URL based on the remote type\n        if ($gitRemote -match \"github.com\") {\n            # Handle GitHub URL\n            $url = \"https://github.com/$githubOrg/$repository/compare/$branchName?expand=1\"\n        }\n        else {\n            Write-Error \"Unsupported git remote URL format: $gitRemote\"\n            exit\n        }\n\n        # Open the URL in Chrome\n        & $chromePath $url\n    }\n    catch {\n        Write-Error \"An error occurred: $_\"\n        exit\n    }\n}\n\n```\n\n## Open solution\n\nThis script searches for a solution (.sln) file in the current directory and its subdirectories.\nIt’s handy when you don’t remember the exact name of the solution file and just need to open the single solution in your repository.\nAlthough it’s limited if there are multiple .sln files in the folder, in most cases you’ll only have one and won’t need to know its exact name.\n\n```powershell\n\nfunction Open-Solution {\n    [Alias(\"os\")]\n    Param ([string] $Pattern, [int] $Depth = 2)\n    if (Test-Path -Path $Pattern -PathType Leaf) {\n        $ExactMatch = Get-ChildItem $Pattern\n        Write-Host \"Opening $($ExactMatch.Name)...\"\n        Invoke-Expression \"& ($ExactMatch.FullName)\" | Out-Null\n        return;\n    }\n    $SingleMatch = Get-ChildItem -Recurse -Filter \"$Pattern.sln\" -Depth $Depth\n    if ($SingleMatch) {\n        Write-Host \"Opening $($SingleMatch.Name)...\"\n        Invoke-Expression \"& ($SingleMatch.FullName)\" | Out-Null\n        return;\n    }\n    $MatchingSlnFiles = Get-ChildItem -Recurse -Filter \"*$Pattern*.sln\" -Depth $Depth\n    if ($MatchingSlnFiles.Count -eq 0) {\n        Write-Host \"No matching solution found\"\n        return;\n    }\n    if ($MatchingSlnFiles.Count -gt 1) {\n        Write-Host \"Multiple matching solutions found:\"\n        $MatchingSlnFiles | ForEach-Object { $_.Name }\n        return;\n    }\n    $matchingSlnFile = $MatchingSlnFiles[0]\n    Write-Host \"Opening $($matchingSlnFile.Name)...\"\n    Invoke-Expression \"& $($matchingSlnFile.FullName)\" | Out-Null\n}\n```\n\n## Measure Time\n\nA very simple script which I have use to perform time-measurements when running scripts.\n\n```powershell\nfunction Measure-Time {\n    [CmdletBinding()]\n    param (\n        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]\n        [ScriptBlock]$ScriptBlock\n    )\n\n    # Start the stopwatch\n    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()\n\n    try {\n        # Execute the script block without capturing the output\n        & $ScriptBlock\n    }\n    finally {\n        # Stop the stopwatch\n        $stopwatch.Stop()\n        # Display the execution time\n        Write-Host \"Execution Time: $($stopwatch.Elapsed.TotalSeconds) seconds\"\n    }\n}\n```\n\nUsage:\n\n```powershell\nMeasure-Time {\n    # Put the code you want to measure here\n    Start-Sleep -Seconds 5\n}\n# Outputs something like: \"Execution Time: 5.003 seconds\"\n```\n\nThats pretty much it, happy coding!\n"},{"slug":"using-dottrace-to-identify-and-improve-performance","title":"Using dotTrace to identify and improve performance","date":"2024-02-13","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/using-dottrace-to-identify-and-improve-performance/","excerpt":"Today we will explore how to enhance the performance of a poorly performing .NET REST API using dotTrace. Specifically, we will delve into an IO bound problem and leverage dotTrace...","content":"\nToday we will explore how to enhance the performance of a poorly performing .NET REST API using dotTrace. Specifically, we will delve into an IO-bound problem and leverage dotTrace's profiling modes, sampling and timeline, to aid in identifying and resolving the bottlenecks.\n\ndotTrace is a performance profiling tool developed by JetBrains for .NET applications. It helps developers diagnose performance issues in their software by providing detailed insights into how well the application is running. dotTrace can profile different types of .NET applications, including desktop, web, and server applications, and it supports various .NET frameworks.\n\nTo get started, you can either download and install dotTrace as a standalone or install the plugin inside Rider IDE or Visual Studio.\nYou can also install both the standalone and plugin edition.\nWe will use both in this example.\n\nWhen the plugin is installed, we can see the following launch options inside the IDE.\n\n![launch options for dotTrace](/images/using-dottrace-to-identify-and-improve-performance/launch-option.png)\n\nHere we have the possibility to choose which profiling mode we should use:\n\n**Sampling Profiling**: Takes periodic snapshots of the application's call stack to identify performance bottlenecks with minimal overhead. Best for initial, high-level performance assessment.\n\n**Timeline Profiling**: Captures real-time events (CPU, memory, threads) over the application's execution timeline. Ideal for analyzing complex issues related to threading or memory spikes.\n\n**Tracing Profiling**: Records detailed information on every function call, including execution times. Suited for in-depth analysis of specific functionalities, but with significant overhead.\n\n**Line-by-Line Profiling**: Offers granular insight by tracking execution time and memory usage for individual code lines. Used for detailed optimization of specific code sections, highest overhead.\n\nNote: Sampling Profiling and Timeline Profiling can attach to an already running process while Tracing and Line-by-Line cannot.\n\nHere is some code from the application that we are going to profile and try to make better based on results from dotTrace:\n\n```csharp\nusing Dapper;\nusing Npgsql;\n\nnamespace ProductApi;\n\npublic interface IProductRepository\n{\n    Task<List<Product>> GetAllProductsAsync();\n    Task<Dictionary<int, string>> GetAllCategoriesAsync();\n    Task<Dictionary<int, string>> GetAllSellersAsync();\n    Task<Dictionary<int, double>> GetAllDiscountsAsync();\n}\n\npublic class ProductRepository : IProductRepository\n{\n    private readonly string _connectionString;\n    \n    public ProductRepository(string connectionString)\n    {\n        _connectionString = connectionString;\n    }\n    \n    public async Task<List<Product>> GetAllProductsAsync()\n    {\n        await using var con = new NpgsqlConnection(_connectionString);\n        await con.OpenAsync();\n\n        const string sql = \"SELECT id, name, price, category_id as CategoryId, seller_id as SellerId, discount_id as DiscountId FROM products\";\n        var products = (await con.QueryAsync<Product>(sql)).ToList();\n        \n        return products;\n    }\n\n    public async Task<Dictionary<int,string>> GetAllCategoriesAsync()\n    {\n        await using var connection = new NpgsqlConnection(_connectionString);\n        await connection.OpenAsync();\n        var categories = await connection.QueryAsync<Category>(\"SELECT id, name FROM categories\");\n        return categories.ToDictionary(x => x.Id, y => y.Name);\n    }\n    \n    public async Task<Dictionary<int, string>> GetAllSellersAsync()\n    {\n        await using var connection = new NpgsqlConnection(_connectionString);\n        await connection.OpenAsync();\n        var sellers = await connection.QueryAsync<Seller>(\"SELECT seller_id as SellerId, name FROM sellers\");\n        return sellers.ToDictionary(x => x.SellerId, y => y.Name);\n    }\n\n    public async Task<Dictionary<int, double>> GetAllDiscountsAsync()\n    {\n        await using var connection = new NpgsqlConnection(_connectionString);\n        await connection.OpenAsync();\n        var sellers = await connection.QueryAsync<Discount>(\"SELECT discount_id as DiscountId, discount_amount as DiscountAmount FROM discount\");\n        return sellers.ToDictionary(x => x.DiscountId, y => y.DiscountAmount);\n    }\n}\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace ProductApi;\n\n[ApiController]\n[Route(\"[controller]\")]\npublic class ProductsController : ControllerBase\n{\n    private readonly IProductRepository _productRepository;\n\n    public ProductsController(IProductRepository productRepository)\n    {\n        _productRepository = productRepository;\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> Get()\n    {\n        var products = await _productRepository.GetAllProductsAsync();\n        var categories = await _productRepository.GetAllCategoriesAsync();\n        var sellers = await _productRepository.GetAllSellersAsync();\n        var discounts = await _productRepository.GetAllDiscountsAsync();\n\n        var response = products.Select(x => new ProductViewModel\n        {\n            Id = x.Id,\n            Category = categories[x.CategoryId],\n            SellerName = sellers[x.SellerId],\n            DiscountAmount = discounts[x.DiscountId],\n            Price = x.Price,\n            Name = x.Name\n        });\n        \n        return Ok(response);\n    }\n}\n\n\n```\n\nBefore we begin with dotTrace, let's take a moment to plan our approach a bit.\nKeep in mind that diving deep into granular-level profiling will undoubtedly give us a wealth of insights but at the expense of greater demand on our application. It might also give us a mountain of details to wade through.\nIt's also important to pinpoint the type of bottleneck we suspect is at play. Are we dealing with delays caused by IO operations, or is the CPU being pushed to its limits? Or maybe it's a mix of both?\n\nGiven that IO operations (in general) take more time compared to CPU operations, it can be good to look into that first.\n\nLet's get a good overview by running the least overhead profile method Sampling Profiling.\nWe can do this by first starting the application and then attaching to the process or launching the application in profile mode.\n\nrunning:\n\n```bash\ncurl -o /dev/null -s -w 'Total time: %{time_total}s\\n' http://localhost:5162/products\n```\n\noutput:\n\n```bash\nTotal time: 4.483009s\n```\n\nWe then will collect the profiling data by clicking:\n\n![get-snapshot](/images/using-dottrace-to-identify-and-improve-performance/get-snapshot.png)\n\nThen, navigate to the other tab to view all snapshots. By right-clicking on a snapshot entry, we can inspect the snapshot:\n\n![open-snapshot](/images/using-dottrace-to-identify-and-improve-performance/open-snapshot.png)\n\nHere we have some options on how to open it:\n\n- **Open Snapshot** - Inside the IDE\n- **Open in standalone dotTrace** - Open it in the standalone edition (if it's installed)\n- **Opening the saved snapshot file** - We can also browse the explorer. Every snapshot is automatically stored to the disk, under the folder %USERPROFILE%\\dotTraceSnapshots[processName] [date]\\\n\nLet's choose Open in standalone dotTrace:\n\n![sampling profile plain text](/images/using-dottrace-to-identify-and-improve-performance/sampling-1.png)\n\nAnd then, let's open up the plain list option to go into a bit more detail:\n\n![sampling profile view plain list](/images/using-dottrace-to-identify-and-improve-performance/sampling-1-plainlist.png)\n\nIn the plain list view, we can see that the most time is spent inside the packages Npgsql, Dapper, and our ProductRepository.\n\nBefore we attempt to improve it, let's use the timeline profiler to see how these database calls are done.\n\nRunning timeline profiling:\n\n![timeline profiler](/images/using-dottrace-to-identify-and-improve-performance/timeline-profiling-1.png)\n\nBy filtering on the User Code subsystem, we can identify that there are four Thread Pool Workers that use the most time.\nWe can also see that they run after each other in series. Inspecting the Plain List, we can identify that it's the calls:\n\n```csharp\nvar products = await _productRepository.GetAllProductsAsync();\nvar categories = await _productRepository.GetAllCategoriesAsync();\nvar sellers = await _productRepository.GetAllSellersAsync();\nvar discounts = await _productRepository.GetAllDiscountsAsync();\n```\n\nBecause they are not dependent calls, let's run them in parallel instead.\n\n```csharp\nvar productsTask = _productRepository.GetAllProductsAsync();\nvar categoriesTask = _productRepository.GetAllCategoriesAsync();\nvar sellersTask = _productRepository.GetAllSellersAsync();\nvar discountsTask = _productRepository.GetAllDiscountsAsync();\n\nawait Task.WhenAll(productsTask, categoriesTask, sellersTask, discountsTask);\n\nvar products = await productsTask;\nvar categories = await categoriesTask;\nvar sellers = await sellersTask;\nvar discounts = await discountsTask;\n```\n\nNow let's profile it with the timeline again:\n\nrunning: \n\n```bash\ncurl -o /dev/null -s -w 'Total time: %{time_total}s\\n' http://localhost:5162/products\n``` \n\noutput:\n\n```\nTotal time: 2.941794s\n```\n\nIt looks like it's a bit faster.\n\nInspecting timeline:\n\n![timeline profiler parallel](/images/using-dottrace-to-identify-and-improve-performance/timeline-profiling-1-parallel.png)\n\nAs we can see in the image above, the thread pool workers start all at the same time, increasing their efficiency.\n\nBut, 2.9 seconds is still too slow for a good user experience.\nLet's have a look at the overall design of the application.\n\nWe get the products and enrich them with data from categories, sellers, and discounts tables.\nDoing this with one call by joining the tables should be much more efficient.\n\nLet's make the following changes:\n\n```csharp\npublic async Task<List<Product>> GetAllProductsAsync()\n    {\n        await using var con = new NpgsqlConnection(_connectionString);\n        await con.OpenAsync();\n\n        const string sql = @\"\n                            SELECT \n\t                            p.id, \n\t                            p.name,\n\t                            p.price,\n\t                            c.name as CategoryName,\n\t                            s.name as SellerName,\n\t                            d.discount_amount as DiscountAmount\n                            FROM products p \n                            INNER JOIN categories as c ON c.id = p.category_id\n                            INNER JOIN sellers as s ON s.id = p.seller_id\n                            INNER JOIN discounts as d ON d.id = p.discount_id\n                            \";\n\n        var products = (await con.QueryAsync<Product>(sql)).ToList();\n        \n        return products;\n    }\n```\n\n```csharp\n[HttpGet]\n    public async Task<IActionResult> Get()\n    {\n        var products = await _productRepository.GetAllProductsAsync();\n        return Ok(products);\n    }\n```\n\nrunning:\n\n```bash\ncurl -o /dev/null -s -w 'Total time: %{time_total}s\\n' http://localhost:5162/products\n```\n\noutput:\n\n```bash\nTotal time: 1.658726s\n```\n\nLets view it with timeline profiling again:\n\n![timeline profiling join fix](/images/using-dottrace-to-identify-and-improve-performance/timeline-profiling-1-join.png)\n\n\nOk, we can see that we are mostly using 1 thread pool worker, but it's more efficient because of the join.\nWe can also note the CPU level is less than previously.\n\nBut 1.6 seconds is still quite a lot.\n\nInspecting the tables, we can see that about 3 million rows are fetched. This is excessive, let's do something about it.\nNo user wants to inspect 3 million rows. We should add pagination to make it more manageable for the user and the server.\n\nLets do the following changes:\n\n```csharp\n   public async Task<List<ProductViewModel>> GetAllProductsAsync(int page, int pageSize)\n    {\n        await using var con = new NpgsqlConnection(_connectionString);\n        await con.OpenAsync();\n\n        // Calculate the number of rows to skip\n        var skip = (page - 1) * pageSize;\n\n        var sql = $@\"\n                    SELECT \n                        p.id, \n                        p.name,\n                        price,\n                        c.name as CategoryName,\n                        s.name as SellerName,\n                        d.discount_amount as DiscountAmount\n                    FROM products p \n                    INNER JOIN categories as c ON c.id = p.category_id\n                    INNER JOIN sellers as s ON s.id = p.seller_id\n                    INNER JOIN discounts as d ON d.id = p.discount_id\n                    ORDER BY p.id\n                    LIMIT {pageSize} OFFSET {skip}\n                    \";\n        var products = (await con.QueryAsync<ProductViewModel>(sql)).ToList();\n    \n        return products;\n    }\n``` \n\n```csharp\n  [HttpGet]\n    public async Task<IActionResult> Get(int page = 1, int pageSize = 10)\n    {\n        var products = await _productRepository.GetAllProductsAsync(page, pageSize);\n        return Ok(products);\n    }\n```\n\nrunning:\n\n```bash\ncurl -o /dev/null -s -w 'Total time: %{time_total}s\\n' \"http://localhost:5162/products?page=1&pageSize=10\"\n```\n\noutput:\n\n```bash\nTotal time: 0.002866s\n```\n\nPerfect!\n\nThat's pretty much it for this post.\n\nHappy coding!\n"},{"slug":"setting-up-kubernetes-job-for-db-migrations-in-dotnet","title":"Setting up DB migrations with .NET running in kubernetes","date":"2023-12-15","tags":["dotnet","kubernetes","devops"],"url":"https://www.rasmusolsson.dev/posts/setting-up-kubernetes-job-for-db-migrations-in-dotnet/","excerpt":"In this guide, we'll start by developing a universal .NET application designed for managing database migrations with DbUp. Our goal is to encapsulate this application within a Dock...","content":"\nIn this guide, we'll start by developing a universal .NET application designed for managing database migrations with DbUp. Our goal is to encapsulate this application within a Docker container and transform it into a reusable Docker image. This image will be engineered to handle various database migrations across different domains, requiring a connection string and SQL scripts folder path as inputs.\n\nFollowing the creation of this generic image, we'll show how it can serve as a foundation for constructing additional images tailored to execute migrations for distinct databases and domains.\n\nThe final step involves deploying and running this as a Kubernetes job.\n\nLets start by setting up a postgres database that will live inside kubernetes\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: postgres-deployment\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postgres\n  template:\n    metadata:\n      labels:\n        app: postgres\n    spec:\n      containers:\n        - name: postgres\n          image: postgres:latest\n          resources:\n            limits:\n              cpu: \"1\"\n              memory: \"1Gi\"\n            requests:\n              cpu: \"0.5\"\n              memory: \"500Mi\"\n          ports:\n            - containerPort: 5432\n          env:\n            - name: POSTGRES_USER\n              value: postgres\n            - name: POSTGRES_PASSWORD\n              value: YourStrong!Passw0rd\n            - name: POSTGRES_DB\n              value: YourDatabaseName\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: postgres-service\nspec:\n  selector:\n    app: postgres\n  ports:\n    - protocol: TCP\n      port: 5432\n      targetPort: 5432\n  type: ClusterIP\n```\n\nthen apply it by running:\n\n```bash\nkubectl apply -f postgres-deployment.yaml\n```\n\nLets verify that everything is setup by connecting to postgres.\n\nNote: Before we connect, because we have ClusterIP, we will have to do port-forwarding on the kubernetes service.\n\n```bash\nkubectl port-forward service/postgres-service 5432:5432\n```\n\nRunning the following to connect to the postgres database:\n\n```bash\npsql -h localhost -p 5432 -U postgres -d YourDatabaseName\n\n```\n\nOutput:\n\n```bash\npsql (16.2)\nType \"help\" for help.\n\nYourDatabaseName=#\n```\n\nPerfect the database connection works! \n\nNow lets create the universal DB migration job with DbUp.\n\n```csharp\nusing DbMigrator;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nvar host = Host.CreateDefaultBuilder(args)\n        .ConfigureAppConfiguration((_, config) =>\n        {\n            config.SetBasePath(Directory.GetCurrentDirectory());\n            config.AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true)\n            .AddCommandLine(args);\n        })\n        .ConfigureServices((hostContext, services) =>\n        {\n            var configuration = hostContext.Configuration;\n            var scriptDirectory = configuration.GetSection(\"DbMigrationSettings\").GetValue<string>(\"ScriptDirectory\");\n            var connectionString = configuration.GetConnectionString(\"PgConnection\");\n            services.AddSingleton(new DbMigrationService(connectionString!, scriptDirectory!));\n        })\n        .Build();\n    \nvar databaseMigrationService = host.Services.GetRequiredService<DbMigrationService>();\ndatabaseMigrationService.MigrateDatabase();\n```\n\n```csharp\nusing DbUp;\n\nnamespace DbMigrator;\n\npublic class DbMigrationService(string connectionString, string scriptDirectory)\n{\n    public void MigrateDatabase()\n    {\n        EnsureDatabase.For.PostgresqlDatabase(connectionString);\n\n        var upgrader = DeployChanges.To\n            .PostgresqlDatabase(connectionString)\n            .WithScriptsFromFileSystem(scriptDirectory)\n            .LogToConsole()\n            .Build();\n\n        var result = upgrader.PerformUpgrade();\n\n        if (!result.Successful)\n        {\n            Console.WriteLine(result.Error);\n        }\n        else\n        {\n            Console.WriteLine(\"Success!\");\n        }\n    }\n}\n```\n\nWe have an appsettings.json that we can use to test with but we will override this configuration later.\n\n```json\n{\n  \"ConnectionStrings\": {\n    \"PgConnection\": \"Server=localhost;Database=YourDatabaseName;User Id=postgres;Password=YourStrong!Passw0rd;\"\n  },\n  \"DbMigrationSettings\": {\n    \"ScriptDirectory\": \"sql\"\n  }\n}\n```\n\nAnd then a simple sql file:\n\n```sql\nCREATE TABLE users (\n   id SERIAL PRIMARY KEY,\n   name VARCHAR(100) NOT NULL,\n   email VARCHAR(150) NOT NULL UNIQUE,\n   joined_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n```\n\nThe project structure looks like this:\n\n```\nDbMigrator\n│   appsettings.json\n│   DbMigrationService.cs\n│   DbMigrator.csproj\n│   Dockerfile\n│   Program.cs\n│\n└───sql\n        Examples.sql\n```\n\nLets first run it locally to see that everything works.\n\noutput:\n\n```\nDbMigrator/bin/Debug/net8.0/DbMigrator.exe \nMaster ConnectionString => Host=localhost;Database=postgres;Username=postgres;Password=******\nBeginning database upgrade\nChecking whether journal table exists..\nJournal table does not exist\nExecuting Database Server script 'Examples.sql'\nChecking whether journal table exists..\nCreating the \"schemaversions\" table\nThe \"schemaversions\" table has been created\nUpgrade successful\nSuccess!\n```\n\nPerfect, it works!\n\nNext we will build an image out of this:\n\n\n```\nFROM mcr.microsoft.com/dotnet/sdk:8.0 AS build\nWORKDIR /src\nCOPY [\"DbMigrator.csproj\", \"./\"]\nRUN dotnet restore \"DbMigrator.csproj\"\nCOPY . .\nRUN dotnet build \"DbMigrator.csproj\" -c Release -o /app/build\n\nFROM build AS publish\nRUN dotnet publish \"DbMigrator.csproj\" -c Release -o /app/publish /p:UseAppHost=false\n\nFROM mcr.microsoft.com/dotnet/runtime:8.0 AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"DbMigrator.dll\"]\n```\n\n```bash\ndocker build -t db-migrator .\n```\n\nNow that we have the universal db migrator image, we have the infrastructure in place to reuse it on multiple projects. Lets have a look how we can do that:\n\n\nBegin by creating a new directory within the specific project requiring a database migration. Within this directory, create a subfolder, possibly named 'sql', and include both a docker-compose file and a Dockerfile looking like this:\n\n```\ndb\n│   docker-compose.yml\n|   dockerfile   \n|\n└───sql\n        payments.sql\n```\n\nExplaination:\n\n- **docker-compose.yml**: Not strictly required, but doing so is helpful for testing locally and for ensuring that all parts are functioning correctly before building the final image\n- **Dockerfile**: Utilized for customizing the db migrator to fit the specific requirements of the project.\n- **sql folder**: Contains the SQL migration scripts for the database\n\n\nLets test the docker-compose file:\n\n```yml\nversion: '3.8'\nservices:\n  db-migrator:\n    image: db-migrator:latest\n    volumes:\n      - .:/app/DbMigrations\n    environment:\n      ConnectionStrings__PgConnection: 'Server=postgres-service;Database=YourDatabaseName;User Id=postgres;Password=YourStrong!Passw0rd;'\n      DbMigrationSettings__ScriptDirectory: 'DbMigrations/sql'\n\n```\n\n```bash\ndocker-compose up\n```\n\noutput:\n\n```\n\\payment> docker-compose up\n[+] Running 1/0\n ✔ Container payment-db-migrator-1  Recreated                                                                  0.1s \nAttaching to db-migrator-1\ndb-migrator-1  | Master ConnectionString => Host=host.docker.internal;Database=postgres;Username=postgres;Password=******\ndb-migrator-1  | Beginning database upgrade\ndb-migrator-1  | Checking whether journal table exists..\ndb-migrator-1  | Fetching list of already executed scripts.\ndb-migrator-1  | Executing Database Server script 'payments.sql'\ndb-migrator-1  | Checking whether journal table exists..\ndb-migrator-1  | Upgrade successful\ndb-migrator-1  | Success!\ndb-migrator-1 exited with code 0\n```\n\nGood!\n\nNext, we will create an image use, we will reuse the universal image and then include script folder inside of it.\n\n```\nFROM db-migrator:latest\n\n# Copy the sql directory into the image\nCOPY ./sql /app/DbMigrations/sql\n```\n\n```bash\ndocker build -t payment-db-migrator .\n```\n\nNow that we have the final image we will wrap it inside a kubernetes job:\n\n```yml\napiVersion: batch/v1\nkind: Job\nmetadata:\n  name: payment-db-migration-job\nspec:\n  template:\n    spec:\n      containers:\n        - name: db-migration\n          image: payment-db-migrator:latest\n          imagePullPolicy: IfNotPresent\n          env:\n            - name: ConnectionStrings__PgConnection\n              value: \"Server=postgres-service;Database=YourDatabaseName;User Id=postgres;Password=YourStrong!Passw0rd;\"\n            - name: DbMigrationSettings__ScriptDirectory\n              value: \"DbMigrations/sql\"\n      restartPolicy: Never\n```\nNote: For production applications, make sure to handle password securely\n\nLets apply the kubernetes job by running: \n```bash\nkubectl apply -f payment-db-migration-job.yml\n```\n\noutput:\n\n```bash\njob.batch/payment-db-migration-job created\n```\n\nLets grab the pod by running: \n\n```bash\nkubectl get pods\n```\n\noutput:\n\n```bash\npayment> kubectl get pods\nNAME                                  READY   STATUS      RESTARTS   \npayment-db-migration-job-qc7gq        0/1     Completed   0          \npostgres-deployment-f847cbbb4-8n2jp   1/1     Running     0          \n```\n\nAnd then check the logs to see if its execute the db migration by running:\n\n```bash\nkubectl logs payment-db-migration-job-qc7gq\n```\n\noutput:\n\n```\nMaster ConnectionString => Host=postgres-service;Database=postgres;Username=postgres;Password=******\nBeginning database upgrade\nChecking whether journal table exists..\nJournal table does not exist\nExecuting Database Server script 'payments.sql'\nChecking whether journal table exists..\nCreating the \"schemaversions\" table\nThe \"schemaversions\" table has been created\nUpgrade successful\nSuccess!\noutput:\n```\n\nPerfect, it works!\n\nHappy coding!"},{"slug":"why-you-might-want-custom-analyzer-rules","title":"Why you might want a custom roslyn analyzer","date":"2023-10-23","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/why-you-might-want-custom-analyzer-rules/","excerpt":"Roslyn analyzer is a tool built on the .NET Compiler Platform which allows developers to perform static code analysis on their .NET projects. These analyzers inspect code for any p...","content":"Roslyn analyzer is a tool built on the .NET Compiler Platform which allows developers to perform static code analysis on their .NET projects.\nThese analyzers inspect code for any potential issues, anything from style, code smells or whatever you define.\nIf you use common IDE:s such as Visual Studio or Rider today, these IDEs leverage the Roslyn compiler platform to enhance your development experience.\nThese IDE:s are by default using the standard configuration, which include a bunch of rules such as:\n\n[CA1822](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1822): Mark members as static to improve performance when the member does not access instance data.\n\nIn some cases, the default rules may not fully meet your needs. For instance, your organization might wish to augment these rules to cater to specific requirements.\nLets say that your tech team or organization only should use DateTime.UtcNow and not DateTime.Now.\nThis can be easily overlooked during a code review. However, if you share a common custom Roslyn analyzer this guideline will be very clear for everyone.\nThe analyzer not only gives you a quick suggestion but can also detailing the rationale behind this rule and the story that led to its adoption.\n\nIn this blog post we will build a custom Roslyn Analyzer.\n\nTo create a Roslyn analyzer there are many different approaches and if you use Visual Studio, you already have pre-configured templates available.\nIn this example I will not use the visual studio template but instead start from scratch.\n\nWe will start by adding a class library for the analyzer:\n\n```bash\ndotnet new classlib -n MyAnalyzer -o MyAnalyzer --framework netstandard2.0\n```\n\nWe will then add the NuGet package that enables us to create an Analyzer:\n\n```bash\ndotnet add package Microsoft.CodeAnalysis.CSharp.Workspaces\n```\n\nA Roslyn analyzer can be divided into two different parts.\n\n1. **Diagnostic**: The first part involves analyzing the code to identify whether it adheres to the defined coding standards or contains specific issues.\n2. **Code Fix**: The second part comes into play once an issue has been identified. This is also where you are provided with a suggested solution.\n\nI have separate this parts into two different classes.\n\nBelow is the Diagnostic class:\n\n```csharp\nusing System.Collections.Immutable;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\nusing Microsoft.CodeAnalysis.Diagnostics;\n\nnamespace MyAnalyzer;\n\n[DiagnosticAnalyzer(LanguageNames.CSharp)]\npublic class AsyncMethodNameAnalyzer : DiagnosticAnalyzer\n{\n    public const string DiagnosticId = \"AsyncMethodName\";\n    private static readonly LocalizableString Title = \"Method name should end with Async\";\n    private static readonly LocalizableString MessageFormat = \"Method '{0}' does not end with Async\";\n    private static readonly LocalizableString Description = \"Async methods should have names ending with Async.\";\n    private const string Category = \"Naming\";\n\n    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(\n        DiagnosticId,\n        Title,\n        MessageFormat,\n        Category,\n        DiagnosticSeverity.Warning,\n        isEnabledByDefault: true,\n        description: Description,\n        helpLinkUri: \"https://github.com/yourusername/yourrepository/blob/main/README.md#asyncmethodname\");\n\n    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);\n\n    public override void Initialize(AnalysisContext context)\n    {\n        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n        context.EnableConcurrentExecution();\n        context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);\n    }\n\n    private static void AnalyzeMethod(SyntaxNodeAnalysisContext context)\n    {\n        var methodDeclaration = (MethodDeclarationSyntax)context.Node;\n        if (methodDeclaration.Identifier.ValueText.EndsWith(\"Async\") || !methodDeclaration.Modifiers.Any(SyntaxKind.AsyncKeyword))\n        {\n            return;\n        }\n\n        var diagnostic = Diagnostic.Create(Rule, methodDeclaration.Identifier.GetLocation(), methodDeclaration.Identifier.ValueText);\n        context.ReportDiagnostic(diagnostic);\n    }\n}\n```\n\n**DiagnosticAnalyzer** - When inheriting from the DiagnosticAnalyzer base class, we give our tool the ability to automatically check the code. You can see it as a guard monitoring for intrusions. Whenever you build or write in the project, this diagnostic analysis will run.\n\n**DiagnosticDescriptor** - The DiagnosticDescriptor is the definition of the analyzer rule. Here, you can specify the severity of the issue and other types of parameters. Another parameter is the helpLink, which can be very useful. Here, you could potentially point to a readme file explaining the background, the decision on why the analyzer exists, and how it works.\n\n**SupportedDiagnostics** - The SupportedDiagnostics property serves as a bridge between our analyzer and the broader Roslyn analysis infrastructure. Here, you can specify a list of rules. In this example, we only have one.\n\n**Initialize** - The Initialize method is the starting point for our analyzer. This is where we specify exactly what our analyzer should be paying attention to within the code. In this method, we register different analysis actions. In our case, we have a method declaration. We do this by using the context.RegisterSyntaxNodeAction method, specifying AnalyzeMethod as the action to perform, and targeting SyntaxKind.MethodDeclaration. The ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None) will ensure we ignore analyzing generated code, which can be problematic or time-consuming. EnableConcurrentExecution basically means that we can run the analyzer on multiple files at once in parallel.\n\n**AnalyzeMethod** - The AnalyzeMethod is the logic used to check whether or not the code adheres to the specified rule.\n\nThe below is the Code Fix class:\n\n```csharp\nusing System.Collections.Immutable;\nusing System.Composition;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CodeActions;\nusing Microsoft.CodeAnalysis.CodeFixes;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\nusing Microsoft.CodeAnalysis.Formatting;\n\nnamespace MyAnalyzer;\n\n[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncMethodNameCodeFixProvider)), Shared]\npublic class AsyncMethodNameCodeFixProvider : CodeFixProvider\n{\n    public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AsyncMethodNameAnalyzer.DiagnosticId);\n\n    public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;\n\n    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)\n    {\n        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);\n        var diagnostic = context.Diagnostics.First();\n        var diagnosticSpan = diagnostic.Location.SourceSpan;\n\n        var declaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault();\n\n        context.RegisterCodeFix(\n            CodeAction.Create(\n                title: \"Add Async suffix\",\n                createChangedSolution: c => AddAsyncSuffix(context.Document, declaration, c),\n                equivalenceKey: \"Add Async suffix\"),\n            diagnostic);\n    }\n\n    private async Task<Solution> AddAsyncSuffix(Document document, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)\n    {\n        var identifierToken = methodDecl.Identifier;\n        var newName = identifierToken.Text + \"Async\";\n\n        var newMethodDecl = methodDecl.WithIdentifier(SyntaxFactory.Identifier(newName)).WithAdditionalAnnotations(Formatter.Annotation);\n        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);\n        var newRoot = root.ReplaceNode(methodDecl, newMethodDecl);\n        var newDocument = document.WithSyntaxRoot(newRoot);\n        return newDocument.Project.Solution;\n    }\n}\n```\n\n**ExportCodeFixProvider** - Ensures that your code fix provider is discoverable.\n\n**FixableDiagnosticIds** - Specifies which diagnostic IDs the code fix provider addresses, connecting the AsyncMethodNameAnalyzer.\n\n**GetFixAllProvider** - Our override of GetFixAllProvider enables us to fix multiple instances of errors at once.\n\n**RegisterCodeFixesAsync** - is the method where you bridge the gap between identifying code issues (via diagnostics) and resolving them (via code fixes).\n\n**AddAsyncSuffix** - The AddAsyncSuffix method automatically appends \"Async\" to the names of asynchronous method declarations lacking this suffix, helping enforce naming conventions. It modifies the syntax tree of the given document and updates the project solution with the renamed method when the fix is applied.\n\nNow that we have a bit better understanding of how the analyzer works, let's try it out.\n\nTo make the analyzer appear, I will create an async function without the async suffix method name:\n\n```csharp\nprivate static async Task Delay()\n{\n    await Task.Delay(1000);\n}\n```\n\nAs we can see, it turns yellow, and when hovered over, it displays\n\n![picture for code fix example 1](/images/why-you-might-want-custom-analyzer-rules/custom-analyzer-1.png)\n\nAnd when pressing the code fix option, we can see 'Add Async Suffix':\n\n![picture for code fix example 2](/images/why-you-might-want-custom-analyzer-rules/custom-analyzer-2.png)\n\nAnd when pressed, the method will be renamed with an 'Async' suffix.\n\nThat's pretty much it. Happy coding!"},{"slug":"behavior-driven-development-with-specflow","title":"Behavior driven development with specflow","date":"2023-08-25","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/behavior-driven-development-with-specflow/","excerpt":"BDD is a technique that enables teams to grasp project requirements clearly through a shared language. This approach ensures that all project participants collaborate effectively o...","content":"BDD is a technique that enables teams to grasp project requirements clearly through a shared language. This approach ensures that all project participants collaborate effectively on solutions. Utilizing the common language of Gherkin (Given, When, Then), it simplifies and clarifies the features of complex projects. BDD enhances communication among stakeholders, including developers, testers, and project owners, who need to work closely together, thereby minimizing the potential for misunderstandings.\n\nIn this post, we will have a brief look how to get started with Behavior driven development with SpecFlow.\n\nSpecFlow is a tool that supports BDD for .NET projects. There are a other BDD tools on the market like [LightBDD](https://github.com/LightBDD/LightBDD) and [NSpec](https://github.com/nspec/NSpec) but [SpecFlow](https://specflow.org/) is considered the most popular.\n\nSpecFlow comes with plugin support for both Visual Studio and Rider IDE.\n\nWe will start by install this through the plugin manager. I'll be using Rider IDE in this example.\n\nGo to settings -> plugin -> search for SpecFlow and click install\n![bdd-plugin-rider](/images/bdd/behavior-driven-development-with-specflow-plugin.png)\n\nNext we will create a Specflow template project.\n\nAdd new project -> search for Specflow and then create.\n\nThis will give us a .net6 csproj (currently) with some Specflow preinstalled packages:\n\n- **TechTalk.SpecFlow** - the official package for Specflow\n- **SpecFlow.NUnit** - allowing us to run SpecFlow on the NUnit test runner and enables you to run your BDD scenarios as NUnit tests.\n- **SpecFlow.Plus.LivingDocPlugin** - enables generating documentation out of the Specflow tests and features.\n\nWhile the template defaults to using NUnit, it's worth noting that you can also select xUnit from the dropdown menu if you prefer. Other test runners, such as MSTest and the SpecFlow+ Runner, are available, but they might not be directly supported in the Rider IDE template.\n\nIn this example, we'll proceed with NUnit as our test runner. However, I plan to explore and compare the SpecFlow+ Runner with NUnit in a future post to highlight their differences and potential advantages.\n\nUpon creating the SpecFlow project template, you'll notice the project structure typically includes several key directories:\n\n- **Driver** - This directory is often used to store the setup code and helpers that drive the tests, acting as a bridge between the step definitions and your application. It's where you'll write code to interact directly with your app, managing state, inputs, and outputs for your tests.\n- **Features** - Here, you'll find the .feature files written in Gherkin syntax. These files describe your application's features and scenarios in a language that's understandable by all stakeholders, serving as the basis for your SpecFlow tests.\n- **Hooks** - Hooks are special methods in SpecFlow that allow you to perform actions at various points in the test execution cycle, such as before or after each scenario or feature. This directory is where you'll define setup and teardown logic that applies across multiple tests.\n- **Steps** - The Steps directory contains the C# bindings for the steps defined in your feature files. Each step in a Gherkin scenario is mapped to a method in these files, containing the code that executes the step. This is where most of your test logic will reside, translating Gherkin steps into actions on your application.\n\nNow that we have the template created, let's implement some code that we will add bdd tests for.\n\nI've added a csproj called game, which includes a player. The player have a action called attack which attacks another player. Based on the player stats and class they have different stats, such as attack power, resistance and health.\n\n```csharp\nnamespace Game;\n\npublic enum CharacterClass\n{\n    Warrior = 1,\n    Mage = 2\n}\n\npublic class Player\n{\n    public Guid Id { get; } = Guid.NewGuid();\n    private int AttackPower { get; }\n    private int Resistance { get; }\n    private CharacterClass CharacterClass { get; }\n    public double Health { get; private set; }\n    \n    public bool IsAlive => Health > 0;\n    \n    public Player(int attackPower, int resistance, double health, CharacterClass characterClass)\n    {\n        AttackPower = attackPower;\n        Resistance = resistance;\n        Health = health;\n        CharacterClass = characterClass;\n    }\n\n    public void Attack(Player enemy)\n    {\n        var damage = CalculateDamage(enemy);\n        if (damage > 0)\n        {\n            enemy.ApplyDamage(damage);\n        }\n    }\n\n    private void ApplyDamage(double damage) => Health -= damage;\n\n    private int CalculateDamage(Player enemy)\n    {\n        var bonusAttackPower = CharacterClass switch\n        {\n            CharacterClass.Mage => 20,\n            CharacterClass.Warrior => 10,\n            _ => 0\n        };\n\n        var enemyBonusResistance = enemy.CharacterClass switch\n        {\n            CharacterClass.Warrior => 10,\n            CharacterClass.Mage => 5,\n            _ => 0\n        };\n\n        var totalAttackPower = AttackPower + bonusAttackPower;\n        var totalEnemyResistance = enemy.Resistance + enemyBonusResistance;\n        \n        return Math.Max(totalAttackPower - totalEnemyResistance, 0);\n    }\n    \n    public void AdjustHealth(double amount)\n    {\n        Health += amount;\n    }\n}\n```\n\nBefore we add some scenarios for this, we should go through what the most common keywords inside a feature file is:\n\n**Feature** - This keyword is used at the very beginning of a feature file to provide a name or a short description of the functionality or feature you are testing. It sets the context for the scenarios that follow.\n\n**Scenario** - Defines a single scenario or user story that you want to test. It's a concrete example of how the feature should work under certain conditions.\n\n**Given, When, Then, And, But** - These are step definitions that describe the steps of each scenario. They help break down the scenario into specific conditions (Given), actions (When), and outcomes (Then). \"And\" and \"But\" can be used for additional conditions, actions, or outcomes.\n\n**Scenario Outline** - This keyword is used for parameterized tests, allowing you to run the same scenario with different sets of values. It's followed by an Examples section that contains a table of parameters to be tested.\n\n**Background** - This keyword allows you to define steps that are common to all scenarios in the feature file. It helps avoid repetition by providing a shared context. Similar to NUnit [SetUp] attribute or XUnit constructor.\n\n**Tags** - Tags categorize scenarios or features for selective execution. Prefixing with tags like @regression or @smoke allows grouping and targeted test runs, enabling flexible test suite management. You can also use @ignore to ignore a feature or scenario to be run.\n\nGoing back to the Player class we can see that there are some significant variation both when it comes to the CharacterClass and the flexible constructor.\nTo accommodate all possible scenarios, using a parameterized approch may be the most sufficient. This is achieved through the use of the \"scenario outline\" keyword.\n\nHere is an example:\n\n![bdd-scenario-outline](/images/bdd/behavior-driven-development-scenario-outline.png)\n\nAfter defining our Scenario Outline in the feature file, the next part is to connect these scenarios to their respective step definitions in SpecFlow. Each Given, When, Then, And, and But in the Scenario Outline corresponds to a method in our test code, known as a step definition, which contains the actual implementation of the scenario.\n\nHere is an example:\n\n```csharp\nusing Game.Specs.Drivers;\nusing NUnit.Framework;\n\nnamespace Game.Specs.Steps;\n\n[Binding]\npublic class PlayerAttackSteps(GameDriver gameDriver)\n{\n    [Given(@\"the Mage has (.*) attack power, (.*) resistance, and (.*) health\")]\n    public void GivenTheMageHasAttackPowerResistanceAndHealth(int attackPower, int resistance, int health)\n    {\n        gameDriver.CreateMage(attackPower, resistance, health);\n    }\n\n    [Given(@\"the Warrior has (.*) attack power, (.*) resistance, and (.*) health\")]\n    public void GivenTheWarriorHasAttackPowerResistanceAndHealth(int attackPower, int resistance, int health)\n    {\n        gameDriver.CreateWarrior(attackPower, resistance, health);\n    }\n    \n    [Then(@\"the Mage's health should be (.*)\")]\n    public void ThenTheMagesHealthShouldBe(double health)\n    {\n        Assert.AreEqual(health, gameDriver.GetMageHealth());\n    }\n    \n    [Then(@\"the Warrior's health should be (.*)\")]\n    public void ThenTheWarriorsHealthShouldBe(double health)\n    {\n        Assert.AreEqual(health, gameDriver.GetWarriorHealth());\n    }\n\n    [When(@\"the Mage attacks the Warrior\")]\n    public void WhenTheMageAttacksTheWarrior()\n    {\n        gameDriver.MageAttacksWarrior();\n    }\n    \n    [When(@\"the Warrior attacks the Mage\")]\n    public void WhenTheWarriorAttacksTheMage()\n    {\n        gameDriver.WarriorAttacksMage();\n    }\n\n    [When(@\"Mage attacks Warrior\")]\n    public void WhenMageAttacksWarrior()\n    {\n        gameDriver.MageAttacksWarrior();\n    }\n    \n    [When(@\"Warrior attacks Mage\")]\n    public void WhenWarriorAttacksMage()\n    {\n        gameDriver.WarriorAttacksMage();\n    }\n}\n```\n\nThe Given, When, Then attributes align with the contents of our feature file. Each time the feature is executed, these elements are parameterized and carried out, enabling our scenarios to flexibly adjust according to the inputs specified in our Examples table.\n\nAs we can see im using a driver, GameDriver. The main ide for a driver is to abstract the complexity of direct interactions with our Player class. This driver acts as a middleman, simplifying the process of setting up tests, executing actions, and verifying outcomes.\n\nHere is an example of a driver:\n\n```csharp\nnamespace Game.Specs.Drivers;\n\npublic class GameDriver\n{\n    private Player Warrior { get; set; }\n    private Player Mage { get; set; }\n\n    public void CreateWarrior(int attackPower, int resistance, double health)\n    {\n        Warrior = new Player(attackPower, resistance, health, CharacterClass.Warrior);\n    }\n\n    public void CreateMage(int attackPower, int resistance, double health)\n    {\n        Mage = new Player(attackPower, resistance, health, CharacterClass.Mage);\n    }\n\n    public void WarriorAttacksMage()\n    {\n        Warrior.Attack(Mage);\n    }\n\n    public void MageAttacksWarrior()\n    {\n        Mage.Attack(Warrior);\n    }\n\n    public double GetWarriorHealth() => Warrior.Health;\n\n    public double GetMageHealth() => Mage.Health;\n\n    public void ResetGameState()\n    {\n        Warrior = null!;\n        Mage = null!;\n    }\n}\n```\n\nSo far we have only covered Scenario Outline because it felt as a good match for our Player class.\nLets have a look at another example that can benefit from keyword \"background\" and regular \"scenario\".\n\nLets say we have the following classes, that are adjusting the player benefits based on the environment:\n\n```csharp\nnamespace Game;\n\npublic class Environment\n{\n    public string Name { get; private set; }\n    private EnvironmentalEffect EnvironmentalEffect { get; set; }\n\n    public Environment(string name, EnvironmentalEffect environmentalEffect)\n    {\n        Name = name;\n        EnvironmentalEffect = environmentalEffect;\n    }\n\n    public void ApplyEffectToPlayer(Player player)\n    {\n        player.AdjustHealth(EnvironmentalEffect.HealthAdjustment);\n    }\n}\n\npublic class EnvironmentalEffect\n{\n    public double HealthAdjustment { get; set; } \n\n    public EnvironmentalEffect(double healthAdjustment = 0.0)\n    {\n        HealthAdjustment = healthAdjustment;\n    }\n}\n\npublic static class GameEnvironments\n{\n    public static readonly Environment HighGround = new(\"HighGround\", new EnvironmentalEffect(healthAdjustment: 10));\n    public static readonly Environment InWater = new(\"InWater\", new EnvironmentalEffect(healthAdjustment: -5));\n}\n```\n\nWhenever a player is InWater there health benefit is reduced by -5 and while on HighGround a health bonus of 10 is added. The feature file for this looks like:\n\n```md\nFeature: Environmental Health Effects\nTo adapt to strategic advantages or challenges,\nPlayers receive health bonuses or suffer penalties based on their environment.\n\n    Background: The player's health benefits is determined by environmental effects\n        Given We have a Mage with 100 health\n    \n    Scenario: Player receives a health bonus on HighGround\n        When the environmental effect HighGround are applied for the Mage\n        Then the player's health should be 110\n\n    Scenario: Player suffers a health penalty in Water\n        When the environmental effect InWater are applied for the Mage\n        Then the player's health should be 95\n```\n\nHere, the Background keyword is useful. We set the mage health to 100 before each test runs allowing us to share a \"given\" between each scenario. We can also see two scenarios that captures both the test for HighGround and InWater which should be enough in this case.\n\nHere is the steps file for the feature:\n\n```csharp\nusing Game.Specs.Drivers;\nusing NUnit.Framework;\n\nnamespace Game.Specs.Steps;\n\n[Binding]\npublic class EnvironmentalHealthEffectsSteps(GameDriver gameDriver)\n{\n    [Given(@\"We have a Mage with (.*) health\")]\n    public void GivenWeHaveAMageWithHealth(int health)\n    {\n        gameDriver.CreateMage(0, 0, health);\n    }\n\n    [When(@\"the environmental effect HighGround are applied for the Mage\")]\n    public void WhenTheEnvironmentalEffectHighGroundAreAppliedForTheMage()\n    {\n        gameDriver.ApplyMageEnvironmentHighGround();\n    }\n\n    [When(@\"the environmental effect InWater are applied for the Mage\")]\n    public void WhenTheEnvironmentalEffectInWaterAreAppliedForTheMage()\n    {\n        gameDriver.ApplyMageEnvironmentInWater();\n    }\n\n    [Then(@\"the player's health should be (.*)\")]\n    public void ThenThePlayersHealthShouldBe(int health)\n    {\n        Assert.AreEqual(health, gameDriver.GetMageHealth());\n    }\n}\n```\n\nNow that we've explored Features, Backgrounds, Scenarios, and Scenario Outlines, let's dig into generating documentation with the SpecFlow.Plus.LivingDocPlugin. This tool will transforms your BDD specifications into an interactive document. The document can be shared with stakeholders to help and ease in viewing the behavior specifications.\n\nFirst, we need to install the SpecFlow.Plus.LivingDoc.CLI dotnet tool. This command-line interface tool allows us to generate the living documentation from our SpecFlow tests. Open your terminal or command prompt and run:\n\n```csharp\ndotnet tool install --global SpecFlow.Plus.LivingDoc.CLI\n```\n\nTo ensure the LivingDoc tool has been installed successfully, enter the following command:\n\n```bash\nlivingdoc --help\n```\n\nThis command should display a list of available commands and options for the livingdoc tool, confirming that it's ready for use.\n\nNow, let's generate the living documentation HTML file. Replace [path to dll for Game.Spec] with the actual path to your SpecFlow project's compiled test assembly DLL file. For example, if your project is named Game.Specs and you've compiled it for .NET 6.0, your path might look something like .\\Game.Specs\\bin\\Debug\\net6.0\\Game.Specs.dll.\n\n```bash\nlivingdoc test-assembly [path to dll for Game.Spec]\n```\n\nThis command will produce an HTML file named LivingDoc.html in the current directory (or the specified output path if you used the --output option).\n\nOpening LivingDoc.html in will present you with view of your project's BDD specifications, including all feature tests and their parameters.\n\nHere is an example image:\n\n![bdd-scenario-outline](/images/bdd/behavior-driven-development-living-doc.png)\n\nNext part could be to integrated this into a CI/CD pipeline job that uploads the living document to a central location, making it easily accessible for all stakeholders.\n\nThats pretty much it for this post.\n\nHappy coding!"},{"slug":"creating-a-net-template-with-template-json","title":"Creating a .NET template with template.json","date":"2023-06-15","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/creating-a-net-template-with-template-json/","excerpt":"In this article, we're going to craft a .NET template together with template.json. Initially, we will develop a simple template for a WebAPI. After that We will introduce an option...","content":"\nIn this article, we're going to craft a .NET template together with template.json. Initially, we will develop a simple template for a WebAPI. After that We will introduce an optional parameter to incorporate a PostgreSQL database connection, and see how template.json can be customized to accommodate this functionality.\n\nLet's start by creating our template solution and an example Web API project:\n\n```bash\ndotnet new sln -n templates\ndotnet new webapi -n WebApiTemplate\ndotnet sln add WebApiTemplate\n```\n\nOpening and inspecting the solution, we should see the following structure (if on .NET 6)\n\n```\nroot\n└───WebApiTemplate\n    │   appsettings.Development.json\n    │   appsettings.json\n    │   Program.cs\n    │   WeatherForecast.cs\n    │   WebApiTemplate.csproj\n    │\n    ├───Controllers\n    │       WeatherForecastController.cs\n    │\n    └───Properties\n            launchSettings.json\n```\n\nAnd now lets create a .template.json file and insert some basic template.json content. (standing at root directory)\n\n```powershell\nNew-Item -Path '.\\WebApiTemplate\\.template.config\\template.json' -ItemType File  -Force\n\nSet-Content -Path '.\\WebApiTemplate\\.template.config\\template.json' -Value @'\n{\n  \"$schema\": \"http://json.schemastore.org/template\",\n  \"author\": \"rasmusolsson.dev\",\n  \"classifications\": [\"Example\", \"Template\", \"Healthcheck\"],\n  \"identity\": \"Example.HealthCheckTemplate.0001\",\n  \"name\": \"My webapi with conventions\",\n  \"shortName\": \"my-webapi\",\n  \"tags\": {\n    \"language\": \"C#\",\n    \"Type\": \"Project\"\n  }\n}\n'@\n```\n\nLet's install and list the template to see how it looks:\n\n```bash\ndotnet new -i .\ndotnet new list --columns-all --author rasmusolsson.dev\n```\n\nOutput:\n\n```\nTemplate Name               Short Name  Language  Type     Author            Tags\n--------------------------  ----------  --------  -------  ----------------  ----------------------------\nMy webapi with conventions  my-webapi   [C#]      project  rasmusolsson.dev  Example/Template/Healthcheck\n```\n\nLet's see how these map against the template.\n\n```\nTemplate Name   => \"name\": \"My webapi with conventions\"\nShort name      => \"shortName\": \"my-webapi\"\nTags            => \"classifications\": [\"Example\", \"Template\", \"Healthcheck\"]\nLanguage        => \"tags\": { \"language\": \"C#\" }\nType            => \"tags\": { \"Type\": \"Project\" }\n```\n\nNow let's use the template we created:\n\n```bash\ndotnet new my-webapi -o temp\n```\n\nThe code for the template minus the .template.config folder should now be available:\n\n```\n├───temp\n│   │   appsettings.Development.json\n│   │   appsettings.json\n│   │   Program.cs\n│   │   WeatherForecast.cs\n│   │   WebApiTemplate.csproj\n│   │\n│   ├───Controllers\n│   │       WeatherForecastController.cs\n│   │\n│   └───Properties\n│           launchSettings.json\n```\n\nNow lets make this abit more complicated.\n\nLets say we want to have options to create a webapi project that uses a postgres database and that its optional.\nWe of course have multiple options here, we can just create another template called for example my-webapi-with-postgres\nor we can use the same template, my-webapi, and parameterize the template.json so that it gives us what we need.\n\nLets go with the second option to explore the template.json further.\n\nWe will start by adding a repository inside the template that will be the access-point to the database.\n\n```powershell\nNew-Item -Path '.\\WebApiTemplate\\Repository\\Repository.cs' -ItemType File -Force\nNew-Item -Path '.\\WebApiTemplate\\Repository\\IRepository.cs' -ItemType File -Force\n\nSet-Content -Path '.\\WebApiTemplate\\Repository\\Repository.cs' -Value @'\nusing System;\nusing Npgsql;\n\nnamespace WebApiTemplate.Repository\n{\n    public class Repository : IRepository\n    {\n        private readonly string _connectionString;\n\n        public Repository(string connectionString)\n        {\n            _connectionString = connectionString;\n        }\n\n        public async Task AddPerson(string name, int age)\n        {\n            using var con = new NpgsqlConnection(_connectionString);\n            con.Open();\n\n            const string sql = \"INSERT INTO people (Name, Age) VALUES (@Name, @Age)\";\n            using var cmd = new NpgsqlCommand(sql, con);\n\n            cmd.Parameters.AddWithValue(\"@Name\", name);\n            cmd.Parameters.AddWithValue(\"@Age\", age);\n            await cmd.ExecuteNonQueryAsync();\n\n            Console.WriteLine(\"Record inserted successfully\");\n        }\n    }\n}\n'@\n\nSet-Content -Path '.\\WebApiTemplate\\Repository\\IRepository.cs' -Value @'\nnamespace WebApiTemplate.Repository;\n\npublic interface IRepository\n{\n    Task AddPerson(string name, int age);\n}\n'@\n```\n\nWe now have the following:\n\n```\n└───WebApiTemplate\n    │   appsettings.Development.json\n    │   appsettings.json\n    │   Program.cs\n    │   WeatherForecast.cs\n    │   WebApiTemplate.csproj\n    │\n    ├───.template.config\n    │       template.json\n    │\n    ├───Controllers\n    │       WeatherForecastController.cs\n    │\n    ├───Properties\n    │       launchSettings.json\n    │\n    └───Repository\n            IRepository.cs\n            Repository.cs\n```\n\nWe also need to add it to dependency injection for the connectionString\n\n```csharp\nvar connectionString = builder.Configuration.GetConnectionString(\"DefaultConnection\");\nbuilder.Services.AddTransient<NpgsqlConnection>(_ => new NpgsqlConnection(connectionString));\nbuilder.Services.AddTransient<IRepository, Repository>();\n```\n\nand also add connectionString to appsettings.json:\n\n```json\n\"ConnectionStrings\": {\n    \"DefaultConnection\": \"Host=localhost;Username=postgres;Password=your_password;Database=SampleDB\"\n  }\n```\n\nand don't forget the .csproj:\n\n```xml\n<PackageReference Include=\"Npgsql\" Version=\"8.0.0-preview.4\" />\n```\n\nIn total there are four different places where we refer to the database in some way:\n\n- repository folder which includes Repository.cs and interface\n- .csproj\n- connectionString in appsettings\n- dependency injection in program.cs\n\nBecause the template should be able to choose if it wants a database we need a parameter for this, lets have a look how we can do this by manipulating the template.json.\n\nthe template.json includes something called symbols, in here we will add a symbol for UseDatabase:\n\n```json\n\"symbols\": {\n    \"UseDatabase\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"bool\",\n      \"defaultValue\": \"false\",\n      \"description\": \"Specifies whether to include database support.\"\n    }\n  }\n```\n\nThis will enable us to use a parameter across our template to include exclude code.\nLets see how we can use it.\n\nWe can use the preprocessor directives: #if (UseDatabase) and #endif to add this code based on if the variable is true or false.\n\n**dependency injection in program.cs**:\n\n```csharp\n#if (UseDatabase)\nusing Npgsql;\nusing WebApiTemplate.Repository;\n#endif\n.\n.\n.\n#if (UseDatabase)\nvar connectionString = builder.Configuration.GetConnectionString(\"DefaultConnection\");\nbuilder.Services.AddTransient<NpgsqlConnection>(_ => new NpgsqlConnection(connectionString));\nbuilder.Services.AddTransient<IRepository, Repository>();\n#endif\n```\n\n**.csproj**: The preprocessor directive is not directly applicable inside the .csproj so we need to use a condition attribute and place it inside for it to be possible. Resulting in:\n\n```csharp\n<PackageReference Condition=\"$(UseDatabase) == true\" Include=\"Npgsql\" Version=\"8.0.2\" />`\n```\n\n**appsettings.json**: The preprocessor directive is not directly applicable here. We will use //(comments) to include the preprocessor directive. Resulting in:\n\n```json\n //#if UseDatabase\n  \"ConnectionStrings\": {\n    \"DefaultConnection\": \"Host=localhost;Username=postgres;Password=your_password;Database=SampleDB\"\n  },\n  //#endif\n```\n\n**repository folder which includes Repository.cs and interface**: Here we could initially think that it should be the same appoach for the program.cs in dependency injection, just add the preprocessor directives, but the folder and files will still be left over. So we will have a look at another solution.\n\nTo be able to exclude content we can use the sources, modifier and add a exclude condition on the UseDatabase. Here is an example:\n\n```json\n\"sources\": [\n    {\n      \"modifiers\": [\n        {\n          \"condition\": \"(!UseDatabase)\",\n          \"exclude\": [\n            \"Repository/**\"\n          ]\n        }\n      ]\n    }\n  ]\n```\n\nNow let's try out our template:\n\n```bash\ndotnet new my-webapi -o MyWebApiWithDatabase --UseDatabase true\n```\n\nOutput:\n\n```\nroot\n│   appsettings.Development.json\n│   appsettings.json\n│   Program.cs\n│   WeatherForecast.cs\n│   WebApiTemplate.csproj\n│\n├───Controllers\n│       WeatherForecastController.cs\n│\n├───Properties\n│       launchSettings.json\n│\n└───Repository\n        IRepository.cs\n        Repository.cs\n```\n\n```bash\ndotnet new my-webapi -o MyWebApiWithoutDatabase --UseDatabase false\n```\n\nNote: We can also omit the --UseDatabase flag completely because it defaults to false\n\nOutput:\n\n```\n│   appsettings.Development.json\n│   appsettings.json\n│   Program.cs\n│   WeatherForecast.cs\n│   WebApiTemplate.csproj\n│\n├───Controllers\n│       WeatherForecastController.cs\n│\n└───Properties\n        launchSettings.json\n```\n\nThats pretty much it for this post!\n\nWe've taken a gentle stroll through the capabilities of template.json, touching just the tip of the iceberg of what's possible. Below are some references to guide you further on your journey into .NET templating, at your own pace.\n\n<https://github.com/dotnet/templating>\n\n<https://learn.microsoft.com/en-us/dotnet/core/tools/custom-templates>\n\n<https://github.com/dotnet/templating/tree/main/dotnet-template-samples>\n\nHappy coding!"},{"slug":"getting-started-with-langchain","title":"Getting started with LangChain","date":"2023-04-27","tags":["GPT","LangChain","typescript"],"url":"https://www.rasmusolsson.dev/posts/getting-started-with-langchain/","excerpt":"LangChain is a tool that helps in the creation of applications that want to fit into an existing Large language models (LLMs). While an LLM is good at answer a general question it...","content":" \nLangChain is a tool that helps in the creation of applications that want to fit into an existing Large language models (LLMs). While an LLM is good at answer a general question it may not be aware of the information, context, or behavior that your application would need.  \nLangChain helps us overcome this by preparing our question so that the LLM produces a viable answer.\n\nA common preparation step in LangChain is:\n\n1. You upload your content\n2. LangChain splits this content into small documents or chunks\n3. Embeds them into a vector database\n\nWhen you provide the question:\n\n1. The question is used to retrieve the most likeliest documents\n2. Send the documents and the question to the LLM.\n3. You receive an answer tailored to your data.\n\nThat is the steps in a nutshell. I will use be using TypeScript in this example.\n\nLet's start by installing all the npm packages that we will be using in this example:\n\n```bash\nnpm i dotenv pdf-parse ts-node typescript langchain @zilliz/milvus2-sdk-node\n```\n\nWe will divide the application into 2 files.\n\n- load.ts, handles the preparation step.\n- index.ts, handles the question and answers.\n\nLet us start with the load phase.\n\n## Load Phase\n\nThis phase refers to the following steps:\n\n1. You upload your content\n2. LangChain splits this content into small documents or chunks\n3. Embeds them into a vector database\n\n### Preparing the data\n\nFirst, create a folder in your working directory called `/data`.\nWe will be placing .pdfs at this location.\nPlace the 3 latest annual reports from Tesla in this location (2022-2020).\n\nYou can find teslas annual reports here: <https://www.annreports.com/tesla>\n\n### Preparing the vector store\n\nWe will be using Milvus as a vector store in the example. Milvus offers a convenient docker-compose file that can be downloaded from <https://milvus.io>. Even though I will provide it here, I really recommend you browse the latest version.\n\n```bash\n\nversion: '3.5'\n\nservices:\n  etcd:\n    container_name: milvus-etcd\n    image: quay.io/coreos/etcd:latest\n    environment:\n      - ETCD_AUTO_COMPACTION_MODE=revision\n      - ETCD_AUTO_COMPACTION_RETENTION=1000\n      - ETCD_QUOTA_BACKEND_BYTES=4294967296\n      - ETCD_SNAPSHOT_COUNT=50000\n    volumes:\n      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd\n    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd\n\n  minio:\n    container_name: milvus-minio\n    image: minio/minio:latest\n    environment:\n      MINIO_ACCESS_KEY: minioadmin\n      MINIO_SECRET_KEY: minioadmin\n    volumes:\n      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data\n    command: minio server /minio_data\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n      interval: 30s\n      timeout: 20s\n      retries: 3\n\n  standalone:\n    container_name: milvus-standalone\n    image: milvusdb/milvus:latest\n    command: [\"milvus\", \"run\", \"standalone\"]\n    environment:\n      ETCD_ENDPOINTS: etcd:2379\n      MINIO_ADDRESS: minio:9000\n    volumes:\n      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus\n    ports:\n      - \"19530:19530\"\n      - \"9091:9091\"\n    depends_on:\n      - \"etcd\"\n      - \"minio\"\n\nnetworks:\n  default:\n    name: milvus\n```\n\n### Preparing env variables\n\nWe will be using dotenv in the example. Please make sure you have a .env file.\nIn .env file need to have the following content:\n\n```bash\nOPENAI_API_KEY=[your-api-key]\nMILVUS_URL=[your-milvus-url] //defaults to http://localhost:19530\n```\n\n### Creating the load.ts script\n\nWe will start by making sure we include .env by performing:\n\n```ts\nimport * as dotenv from 'dotenv';\ndotenv.config();\n```\n\nNext, we will prepare the document loader to load the files that we want to embed into the vector store.\n\n```ts\nconst loader = new DirectoryLoader('./data', {\n    '.pdf': (path) => new PDFLoader(path),\n  });\n```\n\nWhen prepared, we will use a text splitter to divide them into chunks.\n\n```ts\n const docs = await loader.loadAndSplit(\n    new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 100 })\n  );\n```\n\nWe now have the docs. We will now use the Milvus client to upload the documents.\nNote that we are using OpenAIEmbeddings below. That is because we are going to use OpenAI's model.\nIf you planning to use Huggingface or other models, you might require other embeddings.\n\n```ts\n await Milvus.fromDocuments(docs, new OpenAIEmbeddings(), {\n    collectionName: 'tesla_annual_reports',\n  });\n```\n\nThat´s it, we are now done with the load phase.\n\n## Question/answering phase\n\nThis step refers to:\n\n1. The question is used to retrieve the most likeliest documents\n2. Send the documents and the question to the LLM.\n3. You receive an answer tailored to your data.\n\nNext, we will prepare the model. We have a wide range of models to choose from and we can configure them individually.\nI will just use a default OpenAI model in this example.\n\n```ts\nconst model = new OpenAI();\n```\n\nNext we will instantiate a new Milvus Client:\n\n```ts\n const vectorStore = new Milvus(new OpenAIEmbeddings(), {\n    collectionName: 'tesla_annual_reports',\n  });\n```\n\nAfter that, we will prepare the chain. We will use the index-related chain called ConversationalRetrievalQAChain.\n\n```ts\n const chain = ConversationalRetrievalQAChain.fromLLM(\n    model,\n    vectorStore.asRetriever()\n  );\n```\n\nWe now have everything set up in terms of LangChain to make the call.\n\n```ts\nconst res = await chain.call({ question, chat_history: chatHistory });\nconsole.log(res.text);\n```\n\nThat's pretty much it. The `res.text` will contain the response. You can also choose to return the source documents which can help in verifying the response.\n\n## Test\n\nLet's try it out:\n\nQuestion:\n\n```markdown\nWhat was teslas revenue for 2021?\n```\n\nAnswer:\n\n```markdown\n $53,823 million\n```\n\nQuestion:\n\n```markdown\n What are the risks of investing in tesla based on the annual report of 2022?\n```\n\nAnswer:\n\n```markdown\nAccording to the risk factors section in Item 1A, risks of investing in Tesla include competition with other companies for talented employees, difficulty in managing business expansion, and the potential for technological innovations to not be successful.\n```\n\nQuestion:\n\n```markdown\nAre there any additional risks?\n```\n\nAnswer:\n\n```markdown\nThere is a risk that employees may leave Tesla or choose other employers over Tesla due to a competitive labor market. There is also a risk that issues with lithium-ion cells or other components manufactured at Tesla's Gigafactories could harm the production and profitability of Tesla's vehicle and energy storage products. There is also a risk that Elon Musk may have to sell shares of Tesla common stock to satisfy loan obligations or other commitments, which could cause the price of the stock to decline further. Finally, there is a risk that economic incentives may be implemented that provide benefits to domestic manufacturers that do not apply to Tesla, which could negatively impact demand for Tesla's vehicles.\n```\n\nQuestion:\n\n```markdown\nCan you provide the sources for this?\n```\n\nAnswer:\n\n```markdown\nThe sources regarding the risks of investing in Tesla based on the annual report of 2022 are the Risk Factors section (Item 1A) and the Unresolved Staff Comments section (Item 1B).\n```\n\n## Conclusion\n\nLangChain is a very convenient tool to use for adapting an application to work with an existing LLM.\nAs we can see, not a lot of code is required to get something up and I think\nit can open up a lot of interesting opportunities.\n\nI provide the complete source code below:\n\n```ts\n// load.ts\nimport { DirectoryLoader } from 'langchain/document_loaders/fs/directory';\nimport { PDFLoader } from 'langchain/document_loaders/fs/pdf';\nimport { Milvus } from 'langchain/vectorstores/milvus';\nimport { OpenAIEmbeddings } from 'langchain/embeddings/openai';\nimport { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';\n\nimport * as dotenv from 'dotenv';\ndotenv.config();\n\nexport const run = async () => {\n  const loader = new DirectoryLoader('./data', {\n    '.pdf': (path) => new PDFLoader(path),\n  });\n\n  const docs = await loader.loadAndSplit(\n    new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 100 })\n  );\n\n  const res = await Milvus.fromDocuments(docs, new OpenAIEmbeddings(), {\n    collectionName: 'tesla_annual_reports',\n  });\n\n  console.log(res);\n};\n\nrun();\n```\n\n```ts\n//index.ts\nimport { OpenAI } from 'langchain/llms/openai';\nimport { ConversationalRetrievalQAChain } from 'langchain/chains';\nimport { OpenAIEmbeddings } from 'langchain/embeddings/openai';\nimport { Milvus } from 'langchain/vectorstores/milvus';\nimport readline from 'readline';\n\nimport * as dotenv from 'dotenv';\ndotenv.config();\n\nconst rl = readline.createInterface({\n  input: process.stdin,\n  output: process.stdout,\n});\n\nexport const run = async () => {\n  const model = new OpenAI();\n\n  const vectorStore = new Milvus(new OpenAIEmbeddings(), {\n    collectionName: 'tesla_annual_reports',\n  });\n\n  const chain = ConversationalRetrievalQAChain.fromLLM(\n    model,\n    vectorStore.asRetriever()\n  );\n\n  const chatHistory: string[] = [];\n  while (true) {\n    const question = await getUserInput(\n      'Please enter your input or type \"exit\" to quit: '\n    );\n\n    if (!question) {\n      break;\n    }\n\n    const res = await chain.call({ question, chat_history: chatHistory });\n    console.log(res.text);\n    chatHistory.push(question + res.text);\n  }\n};\n\nconst getUserInput = async (prompt: string): Promise<string | false> => {\n  return new Promise((resolve) => {\n    rl.question(prompt, (input) => {\n      if (input === 'exit') {\n        rl.close();\n        resolve(false);\n      } else {\n        resolve(input);\n      }\n    });\n  });\n};\n\nrun();\n\n\n```\n\nHappy coding!\n"},{"slug":"chat-gpt-news-mars-2023","title":"OpenAI launches GPT-4, plus subscription and plugins","date":"2023-03-24","tags":["GPT"],"url":"https://www.rasmusolsson.dev/posts/chat-gpt-news-mars-2023/","excerpt":"Introduction OpenAI is launching new features super fast right now and we as developer can benefit greatly in keeping up to date with this generative ai trend. This post will inclu...","content":"\n## Introduction\n\nOpenAI is launching new features super fast right now and we as developer can benefit greatly in keeping up to date with this generative ai trend. This post will include the biggest announcement from OpenAI for February and Mars.\n\n## Plus Subscription\n\nIn February ChatGPT introduced Plus Subscription for 20$ per month.\n\nBack then the Plus Subscription included:\n\n- General access to ChatGPT, even during peak times\n- Faster response times\n- Priority access to new features and improvements\n\nAdditionally, we will now also gain access to the new GPT-4 model that released a few weeks ago.\nThe GPT-4 model is limited to 25 message per 3 hours though, so keep that in mind.\n\n## The launch of GPT-4\n\nWith the launch of GPT-4, OpenAI has greatly increase the following number of properties:\n\n|<div style=\"width:180px;\">Type</div>|<div style=\"width:120px;\">GPT-4</div>|GPT-3.5 (text-davinci-003)|\n|:---------------|:--------------|:---------------|\n|Nr Of parameters   |   1 trillion |   175 billion |\n|Max tokens         |   8192       |   4096        |\n|Cut of date        |   Sept 2021  |   June 2021   |\n\nNote that this is based on the ChatGPT website and the max token limits can be increased even further through the OpenAI API using these models.\n\nThe only downside right now is that you require a subscription, the limit of message 25 per third hour and that it's a slower than GPT-3.5.\n\n## Plugin\n\nAnother big topic is that OpenAI launching Plugins for ChatGPT.\nThis is plugins that your ChatGPT conversation will have access to.\nYou can now sign up for the waiting list (Link in references)\n\n## Conclusion\n\nThe unveiling of the GPT-4 model significantly enhances the value of the Plus Subscription, making it more worth the investment. The model feels better in all cases aside from speed and with plugin integration soon available. Im sure it will be part of the Plus Subscription later this year.\n\n## References\n\n- OpenAI launching GPT-4: <https://openai.com/research/gpt-4>\n- OpenAI launching ChatGPT Plus subscription: <https://openai.com/blog/chatgpt-plus>\n- OpenAI launching Plugins <https://openai.com/blog/chatgpt-plugins>\n\nHappy coding!\n"},{"slug":"tracking-nuget-licenses-with-dotnet-project-licenses","title":"Tracking nuget licenses with dotnet-project-licenses","date":"2023-02-21","tags":["dotnet","devops"],"url":"https://www.rasmusolsson.dev/posts/tracking-nuget-licenses-with-dotnet-project-licenses/","excerpt":"Introduction When developing software application we include NuGet packages which in turn has software licenses. This license describes how your company should comply when using th...","content":"\n## Introduction\n\nWhen developing software application we include NuGet packages which in turn has software licenses.\nThis license describes how your company should comply when using this software.\nKeeping track of these licenses can be a challenge.\n\nLet's take an example and look at the NuGet package ImageSharp. A common library used for handling images in .NET.\nIn version 2.x, the license model was Apache-2.0.\nWhen bumping this package to 3.x a new license model is used that might require you company to pay for a commercial license.\n\nKeeping track of these can be a daunting task and in this post we will be looking at a tool called dotnet-project-license and how it can help us with this.\n\n## dotnet-project-license\n\nLets start by installing dotnet-project-license:\n\n```bash\ndotnet tool install --global dotnet-project-licenses  \n```\n\nWhen installed, we can run the --help command:\n\n```bash\ndotnet-project-licenses --help \n```\n\nThis will provide a long list of different options. We will be using the following flags in this example:\n\n- --input: The input, folder/.csproj/.sln or a json file containing a list of projects\n- --json: Writes the licenses as and to a json file.\n- --package-filters: A way to skip certain packages. We will skip some microsoft packages.\n- --transient: Includes transient dependencies\n- --unique: Unique licenses list by Id/Version\n- --output-directory: Where to place the licenses.json file.\n\nLets start with a basic command:\n\n```bash\ndotnet-project-licenses --input [.sln] --json\n```\n\nA licenses.json file will be created.\nWhen inspecting this file we can see a JSON array with a license objects inside. For example:\n\n```json\n[\n{\n    \"PackageName\": \"coverlet.collector\",\n    \"PackageVersion\": \"3.2.0\",\n    \"PackageUrl\": \"https://github.com/coverlet-coverage/coverlet\",\n    \"Copyright\": \"\",\n    \"Authors\": [\"tonerdo\"],\n    \"Description\": \"Coverlet is a cross platform code coverage library for .NET, with support for line, branch and method coverage.\",\n    \"LicenseUrl\": \"https://licenses.nuget.org/MIT\",\n    \"LicenseType\": \"MIT\",\n    \"Repository\": {\n      \"Type\": \"git\",\n      \"Url\": \"https://github.com/coverlet-coverage/coverlet.git\",\n      \"Commit\": \"e2c9d84a84a9d2d240ac15feb70f9198c6f8e173\"\n    }\n  }\n]\n```\n\nSome of the flags for dotnet-project-licenses require JSON files as input. Let's create a folder called /licenses that can contain all of these.\n\nNext, let's create a file called packages-filter.json. This file will be used to filter away packages that we are not going to license check. It uses an array with strings inside to filter away certain packages:\n\n```json\n[\"Microsoft.NET.Test.Sdk\", \"\"]\n```\n\nThe same approach is used for the following flags:\n\n- --forbidden-license-types\n- --allowed-license-types\n- --projects-filter\n- --manual-package-information\n\nNow let's use the full command:\n\n```bash\ndotnet-project-licenses --input [.sln] --packages-filter ./licenses/packages-filter.json --unique --transient --json  --output-directory ./licenses\n```\n\nWe now have a licenses.json file contained within our ./licenses directory.\n\n## CI integration\n\nTo prevent the unnoticed inclusion of new licenses, we can add this file to our source code and validated it in our continuous integration process. This ensures that if any disparity arises between the licenses.json file and the one produced during the CI, the pipeline is halted and developer has to review the new licenses before proceeding.\n\n</br>\nHappy Coding!\n"},{"slug":"how-i-use-and-choose-not-to-use-chat-gpt","title":"How I Use and Choose Not to Use ChatGPT","date":"2023-01-24","tags":["GPT","productivity"],"url":"https://www.rasmusolsson.dev/posts/how-i-use-and-choose-not-to-use-chat-gpt/","excerpt":"Introduction So much have happened when it comes to generative AI lately and a huge part of this is due to the launch of ChatGPT. I have been using it on a daily basis both for pri...","content":"\n## Introduction\n\nSo much have happened when it comes to generative AI lately and a huge part of this is due to the launch of ChatGPT. I have been using it on a daily basis both for private and work related matters. In this post I will share with you how I use it and why it makes me more productive.\n\n## ChatGPT privacy concerns\n\nChatGPT promises not to share your data with anyone else, and it says that OpenAI employees need special permission to look at your conversations. Still, it's really important for you to understand what your workplace expects.\n\nI've requested and obtained a detailed document featuring guidelines such as the prohibition of sharing code that might reveal company secrets. It's wise to sort this out initially, so you're aware of the permissible and impermissible uses of ChatGPT in your work environment.\n\n## Communication\n\nWhen interacting with customers or external parties, I often revise and refine my text. This not only ensures that I don't overlook any details, but it also guarantees that my messages are clear, concise and easy to understand.\nChatGPT help me iterate on this more efficiently.\n\nSome commands that I use:\n\n- `Rewrite the following:`\n- `Rewrite the following in the [tone]`\n- `Rewrite the following so that it fit [situation]:`\n\nWhen providing the content, I ensure that any company-specific information is omitted, thereby maintaining a general level of detail that goes in line with the company privacy and ChatGPT policy.\n\nFinally when ChatGPT respond, I look over the content. If it's not good enough, I try add a bit more context to what I think could be improved. When its finally good enough I conduct last manual adjustment and fills in any omitted information.\n\n## Boilerplate's\n\nBecause ChatGPT cut of date is june 2020 the most up to date solutions might not be possible to find.\nBut some questions are still relevant. Lets say I would like to create a bash script doing the following:\n\n1. Retrieve the comma separated list as parameters\n2. Call the executable in a loop for each element\n3. Write down the result to a file\n\nThis script could take a few minutes to get in place but with ChatGPT you could save those minutes by using the produced boilerplate.\n\n## Code changes\n\nThis one can be a bit hard to pull off because of policy concerns.. but If your company allows you to put in isolated methods or files that by themselves does not exposes any company secrets, refactoring can be very powerful. Some example commands:\n\n- Convert the following JavaScript file to TypeScript\n- Convert the following imperative code to be functional\n- Add test scenarios for the following function by using this [framework/tools]\n- Add regexp based on the following rules\n- Add additional methods for the following code that does [this]\n\nThe number of use-cases here is unlimited.\nThe code modifications shared can greatly differ in quality, so my typical strategy is to use them to generate a basic code structure swiftly, and then progressively perfect it.\n\n## Other things I sometimes using ChatGPT for\n\n- Documentation. Produce documentation for the following in markdown format\n- Summarize the following. In example public customer feedback or an article\n- General googling but for ChatGPT\n\n## What Im not using ChatGPT for\n\n- Cut off date. Im not using ChatGPT for solutions that requires up-to-date information.\n- A lot of context. The max number of tokens aka context length is 4096 tokens. Which makes it hard for ChatGPT to produce useful responses.\n- Privacy concerns, both based on the company policies and sensitive personal information.\n- Blog posts. I put much effort in my own reflections and thoughts. I might use the rewrite strategy on a paragraph or two but the overall post will be from my own words.\n\nHappy Coding!\n"},{"slug":"advent-of-code-2022","title":"Advent of code 2022","date":"2022-12-28","tags":["GPT","AOC"],"url":"https://www.rasmusolsson.dev/posts/advent-of-code-2022/","excerpt":"As I wrote in my previous post, I would like to test ChatGPT for advent of code challenges this year and this post is about my initial thoughts. Alright, let's dive straight in and...","content":"\nAs I wrote in my previous post, I would like to test ChatGPT for advent of code challenges this year and this post is about my initial thoughts.\n\nAlright, let's dive straight in and tackle day 4 with it.\n\nI started by calibrating ChatGPT to be aware of the goal of this conversation.\n\nMe:\n\n```markdown\nI need help to answer a advent of code question. I will provide you with the question and its up to you to give back javascript code that can be used to solve the question. Make sure that your answer matches with the example provided in the question. Are you ready?\nPlease note that I will divide the question and send it in chunks to you. I will tell you when the complete question has been transferred. \n```\n\nChatGPT:\n\n```markdown\nYes, I'm ready to help you with your Advent of Code question. Please go ahead and provide the first part of the question. Let me know when you have transferred the complete question, and I'll do my best to provide you with the JavaScript code to solve it.\n```\n\n\nI gradually provide the entire question, piece by piece. As I was feeding segments of the question, ChatGPT was progressively constructing the script. Take for instance:\n\nMe:\n\n```markdown\nThe first time a marker appears is after the seventh character arrives. Once it does, the last four characters received are jpqm, which are all different. In this case, your subroutine should report the value 7, because the first start-of-packet marker is complete after 7 characters have been processed.\n```\n\nChatGPT:\n\n```markdown\nApologies for the oversight. I have revised the code to accurately detect the first occurrence of a complete start-of-packet marker after processing a given number of characters. Here's the updated code:\n```\n\n```js\nfunction findMarkerLength(datastream) {\n  const markerLength = 4; // Length of the start-of-packet marker\n\n  for (let i = markerLength - 1; i < datastream.length; i++) {\n    const marker = new Set(datastream.slice(i - markerLength + 1, i + 1));\n\n    if (marker.size === markerLength) {\n      return i - markerLength + 1; // Return the position of the first four-character marker\n    }\n  }\n\n  return -1; // Return -1 if the marker is not found\n}\n```\n\n```markdown\nNow, let's use the updated code to find the position of the first start-of-packet marker in the given datastream buffer:\n```\n\n```js\nconst datastream = \"mjqjpqmgbljsphdztnvjfqwrcgsmlb\";\nconst markerPosition = findMarkerLength(datastream);\n\nif (markerPosition !== -1) {\n  const numCharacters = markerPosition + markerLength;\n  console.log(\"Number of characters to the end of the marker:\", numCharacters);\n} else {\n  console.log(\"Marker not found.\");\n}\n```\n\n```markdown\nWhen executing this revised code, it will output the following: Number of characters to the end of the marker: 7\n\nIn this case, the first complete start-of-packet marker \"jpqm\" appears after processing the seventh character. Therefore, the correct result is 7, representing the number of characters from the beginning of the buffer to the end of the marker.\n\nI apologize for any confusion caused by the previous responses. Please let me know if you have any further questions or need additional assistance!\n```\n\nWhen the transfer of question was completed. I finally asked it for the complete script.\n\nMe:\n\n```markdown\nPlease provide me with the complete code\n```\n\nChatGPT:\n\n```js\nfunction findMarkerLength(datastream) {\n  const markerLength = 4; // Length of the start-of-packet marker\n\n  for (let i = markerLength - 1; i < datastream.length; i++) {\n    const marker = new Set(datastream.slice(i - markerLength + 1, i + 1));\n\n    if (marker.size === markerLength) {\n      return i - markerLength + 1; // Return the position of the first four-character marker\n    }\n  }\n\n  return -1; // Return -1 if the marker is not found\n}\n\nconst examples = [\n  \"bvwbjplbgvbhsrlpgdmjqwftvncz\",\n  \"nppdvjthqldpwncqszvftbrmjlhg\",\n  \"nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg\",\n  \"zcfzfwzzqfrljwzlrfnpqdbhtmscgvjw\"\n];\n\nfor (const example of examples) {\n  const markerPosition = findMarkerLength(example);\n\n  if (markerPosition !== -1) {\n    const numCharacters = markerPosition + 4; // markerLength = 4\n    console.log(`Number of characters for \"${example}\":`, numCharacters);\n  } else {\n    console.log(`Marker not found for \"${example}\".`);\n  }\n}\n```\n\nAnd when I provided the complete puzzle input it worked. Magic!\n\nBeing upfront, this process doesn't seem work for most questions. The main problem seem to lies in the parsing of examples inputs or interpreting the intent. But with the right amount of human guidance, it can greatly speed things up things, particularly in the case of boilerplate's.\n\nI'm gonna write another post about how I use ChatGPT in my every day life to crank up my efficiency. Stay tuned!\n\nHappy Coding!\n"},{"slug":"chat-gpt-launching","title":"ChatGPT is launching!","date":"2022-11-30","tags":["GPT","AOC"],"url":"https://www.rasmusolsson.dev/posts/chat-gpt-launching/","excerpt":"ChatGPT is launching today and in a perfect timing for Advent of code that begins tomorrow. I would like to try if its smart enough to solve advent of code for me. I've recently tr...","content":"\nChatGPT is launching today and in a perfect timing for Advent of code that begins tomorrow.\n\nI would like to try if its smart enough to solve advent of code for me.\nI've recently tried using github copilot on a last years question and it was quite bad at it. I think ChatGPT is on another level but it probably depends completely on how we describe the problem for it.\n\nRemember being a software developer is so much more then just writing code.. but instead of neglecting it because we are afraid, we should join them. \"If you can't beat them join them\"...  So, maybe ChatGPT can be a useful tool like github copilot that we can use to be more efficient, we'll see..\n\nIf you haven't had the chance to check out the blog post yet.\n\nCheck it out here: <https://openai.com/blog/chatgpt>\n\nAdvent of Code: <https://adventofcode.com/2022/>\n\nHappy Coding!\n"},{"slug":"enforcing-architectural-rules-with-netarchtest","title":"Enforcing Architectural rules with NetArchTest","date":"2022-06-30","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/enforcing-architectural-rules-with-netarchtest/","excerpt":"Independently where you work, there is solution architectural standards that need to followed and enhanced. There might be guidelines, such as avoiding the inclusion of contract de...","content":"\nIndependently where you work, there is solution architectural standards that need to followed and enhanced. There might be guidelines, such as avoiding the inclusion of contract dependencies in the repository layer or ensuring that files implementing an interface have a 'command' suffix in their names.\n\nWhile documenting and communicating these rules collectively may be sufficient in some cases, the risk of non-standard implementations increases as the company expands and more teams become involved. In such situations, it would be beneficial to have a framework that assists developers in adhering to the solution design standards.\n\nOne resource that can assist in this process is NetArchTest, a .NET library designed for implementing and enforcing architectural guidelines. By integrating this library with unit tests, it ensures that any violation of architectural principles is flagged and explained, leading to a failed test.\n\nLets look at an example.\n\nLets say we have the following design rules to adhere to:\n\n- All repositories should be located in the repository folder.\n- Contracts should not be directly referenced in repositories\n- All repositories should have a name-suffix of repository\n\nLets apply this rules to the following solution.\n\n![Enforcing architectural rules sln structure](/images/enforceing-convensions-with-netarctest/enforcing-architectural-rules-structure.png)\n\nFirst of I want to add some extensions methods that defines the different rules.\n\n```csharp\n\npublic static class PolicyExtensions\n{\n    public static PolicyDefinition RepositoriesShouldBeLocatedInRepositoriesFolder(this PolicyDefinition policyDefinition)\n    {\n        return policyDefinition.Add(t =>\n                t.That()\n                    .AreClasses()\n                    .And()\n                    .HaveNameMatching(\"Repository\")\n                    .Or().ImplementInterface(typeof(IRepository))\n                    .Should()\n                    .ResideInNamespace(\"Infrastructure.Repositories\"),\n            \"Repositories should be located in the repository folder.\", \n            \"To ensure repositories are easy to locate\");\n    }\n    \n    public static PolicyDefinition RepositoriesShouldHaveNamingSuffixRepository(this PolicyDefinition policyDefinition)\n    {\n        return policyDefinition.Add(t =>\n                t.That()\n                    .ResideInNamespace(\"Infrastructure.Repositories\")\n                    .Should().HaveNameEndingWith(\"Repository\"),\n            \"All repositories should have a name-suffix of repository \",\n            \"To comply with the naming conversions for repositories.\");\n    }\n    \n    public static PolicyDefinition ContractsShouldNotBeReferencedInsideRepositories(this PolicyDefinition policyDefinition)\n    {\n        return policyDefinition.Add(t =>\n                t.That()\n                    .ResideInNamespace(\"Infrastructure.Repositories\")\n                    .ShouldNot()\n                    .HaveDependencyOn(\"Contracts\"),\n            \"Contracts should not be directly referenced in repositories\",\n            \"A separation between presentation and persistence\"\n        );\n    }\n}\n```\n\nIt is relatively simple to incorporate methods like these. I will now establish a new policy and apply these rules.\n\n```csharp\n public static readonly PolicyDefinition MyPolicies =\n        Policy.Define(\"My policies\", \"This policies defines the architectural rules\")\n            .For(Types.InAssembly(typeof(IRepository).Assembly))\n            .ContractsShouldNotBeReferencedInsideRepositories()\n            .RepositoriesShouldHaveNamingSuffixRepository()\n            .RepositoriesShouldBeLocatedInRepositoriesFolder();\n```\n\nNow lets execute a unittest that is using this policy.\n\n```csharp\n [Test]\n    public async Task Should_follow_the_architectural_rules()\n    {\n        var policyResults = Policies.MyPolicies.Evaluate();\n\n        await Policies.ReportAsync(policyResults, Console.Out);\n        Assert.False(policyResults.HasViolations);\n    }\n```\n\nIf the rules fail, I've used a console log that will show which types are not adhering to the rules. Running this on the following solution would result 3 rule violations. It will look like this:\n\n![Enforcing architectural rules test output](/images/enforceing-convensions-with-netarctest/enforcing-architectural-rules-testoutput.webp)\n\nThat pretty much it.\n\nConclusion. To enforce solution design structure by using unittest is not something new. While you could write your own generic code library to do this (like many other libraries), NetArchTest is a well written, easy to use, and comes with very readable interface that gets you going in minutes.\n\nMore info about NetArchTest can be found here:\n[https:/github.com/BenMorris/NetArchTest](https:/github.com/BenMorris/NetArchTest)\n\nHappy Coding!\n"},{"slug":"productive-with-tampermonkey","title":"Increase your productivity with Tampermonkey","date":"2022-03-25","tags":["javascript","productivity"],"url":"https://www.rasmusolsson.dev/posts/productive-with-tampermonkey/","excerpt":"In earlier posts, I've discussed enhancing productivity with PowerShell and Git, and Tampermonkey is yet another valuable instrument to include in your arsenal of productivity tech...","content":"In earlier posts, I've discussed enhancing productivity with PowerShell and Git, and Tampermonkey is yet another valuable instrument to include in your arsenal of productivity techniques.\n\nTampermonkey is a browser extension that enables the execution of userscripts. Userscripts are compact JavaScript code snippets designed to run in your browser and assist with automating tasks.\n\nAs userscripts run within the browser's current window, they offer numerous practical applications. For instance, suppose you're waiting for a CI pipeline to complete. You could either wait for the build to finish or attempt to be productive by working on something else, but you'd still need to periodically check in to see if the build is done. Tampermonkey is perfect for this situation. You can write a userscript and receive notifications via the SpeechSynthesis voice API when the build is complete. Let's explore how to develop a userscript that can assist us in achieving this:\n\n\n```js\n// ==UserScript==\n// @name         DOM Element Notifier\n// @namespace    http://tampermonkey.net/\n// @version      0.1\n// @description  Waits for a specific element to appear in the DOM and announces its readiness using the SpeechSynthesis API\n// @author       You\n// @match        https://news.ycombinator.com\n// @grant        none\n// ==/UserScript==\n\n(function() {\n    'use strict';\n     // Replace the CSS selector below with the one you want to monitor\n    const targetElementSelector = '#hnmain > tbody > tr:nth-child(1) > td > table > tbody > tr > td:nth-child(2) > span > b > a';\n    const checkInterval = 5000; // Check interval in milliseconds (e.g., 5000 ms = 5 seconds)\n\n    let timeoutId;\n\n    function speak(text) {\n        const message = new SpeechSynthesisUtterance(text);\n        window.speechSynthesis.speak(message);\n    }\n\n    function checkForElement(selector) {\n        console.log('Checking for target element...');\n        if (document.querySelector(selector)) {\n            console.log('Target element found.');\n            speak(`DOM element is ready. Do you thing!`);\n            clearTimeout(timeoutId);\n        } else {\n            console.log('Target element not found. Will check again after the interval.');\n            timeoutId = setTimeout(() => {\n                checkForElement(selector);\n            }, checkInterval);\n        }\n    }\n\n    function startChecking() {\n        checkForElement(targetElementSelector);\n    }\n\n    function addButton() {\n        const button = document.createElement('button');\n        button.textContent = 'Start Monitoring';\n        button.style.position = 'fixed';\n        button.style.top = '10px';\n        button.style.left = '10px';\n        button.style.zIndex = '9999';\n        button.style.padding = '10px';\n        button.style.fontSize = '16px';\n        button.style.cursor = 'pointer';\n        button.onclick = startChecking;\n\n        document.body.appendChild(button);\n    }\n\n    addButton();\n})();\n```\n\n<br />\nIn the script, you'll notice that I'm incorporating a button. This addition was not required earlier, but since the release of Chrome 71, user interaction has become necessary for enabling the SpeechSynthesis voice API.\n\nBy clicking this button, it will initiate monitoring for the specific element and notify me when it is ready.\n\n<br />\nThats pretty much it.\n\nHappy Coding!\n"},{"slug":"stay-productivity-with-formatted-code","title":"Stay productive with formatted code","date":"2022-02-25","tags":["dotnet","productivity","devops"],"url":"https://www.rasmusolsson.dev/posts/stay-productivity-with-formatted-code/","excerpt":"It has always surprised me that the demand for a unified code format is not that desirable in dotnet. In Node with the support for eslint and prettier, setting up formatting and li...","content":"\nIt has always surprised me that the demand for a unified code format is not that desirable in dotnet. In Node with the support for eslint and prettier, setting up formatting and linting rules is almost mandatory. For dotnet, formatting and linting still seem to be optional in most developers setup.\n\nThe benefits of using a unified code format for a development department can be quite low-hanging fruit and increase productivity. Reading code that looks the same, aka has the same format, saves us time, reduces noise during code reviews, and lets us focus more on functionality and less on subjective rules.\n\nIn this post, I will go through how to set up dotnet format.\n\n## Determine the format rules\n\nTo determine what formatting rules to use at your company/team will probably be the most demanding step.\nEven though there are good defaults in dotnet format, there may still be some customizations that your department wants to make.\n\nTo add a dotnet format customization, you can create a .editorconfig.json file in your solution.\ndotnet format will then look for this file when formatting.\n\nMore on editorconfig.json can be read here:\n\n<https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#editorconfig>\n\n## Guard from unformatted code changes\n\nWhen you have determined the rules to use, you would want to guard that no new unformatted changes make it into your main branch. This step may vary depending on your CI setup, but adding a separate linting step before you build your solution would probably be the most likeliest place.\n\nTo check the format, run the following command:\n\n```bash\ndotnet format --verify-no-changes\n```\n\nThis will result in a non-zero status code if the solution isn't already formatted.\n\nTo make sure that CI has dotnet format available, you can use a tool-manifest file. This will also help in that dotnet format runs on the same version for local development.\n\nYou basically creates a manifest file and then run dotnet tool restore. More information about a local manifest file can be found here:\n\n<https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-local-tool>\n\n## Run dotnet format\n\nThis step includes formatting the code.\nBefore running dotnet format, it might be necessary to double-check that dotnet format is excluding folders you don't want to have formatted. In example generated files. You can use the --exclude parameter for that but other than that it should just be to run:\n\n```bash\ndotnet format\n```\n\n## Run dotnet format on client commit hook\n\nDepending on your development process you may want to run dotnet format on git commit. You can read more about that in my git hook for dotnet post here:\n<https://www.rasmusolsson.dev/posts/git-hook-for-dotnet>\n\nThat's pretty much it.\n\nHappy Coding!\n"},{"slug":"advent-of-code-2021","title":"Advent of Code 2021","date":"2022-01-05","tags":["aoc","deno","typescript"],"url":"https://www.rasmusolsson.dev/posts/advent-of-code-2021/","excerpt":"Advent of Code 2021 has come to an end. I decided to go with TypeScript on Deno, the same approach as last year, which I think works great for these kinds of puzzles. I didn't mana...","content":"\nAdvent of Code 2021 has come to an end.\n\nI decided to go with TypeScript on Deno, the same approach as last year, which I think works great for these kinds of puzzles. I didn't manage to finish all puzzles though. I went off on a vacation in December skiing and I didn't have the time to catch up afterward.\n\nHowever, if you are interested to look at the effort, feel free to have a look at my repo below.\n\nGithub: <https://github.com/raholsn/advent-of-code>\n\nAdvent of Code: <https://adventofcode.com/2021/>\n\nHappy Coding!\n"},{"slug":"extend-your-toolkit-with-benchmarks","title":"Extend your toolkit with benchmarks","date":"2021-12-25","tags":["dotnet","performance","devops"],"url":"https://www.rasmusolsson.dev/posts/extend-your-toolkit-with-benchmarks/","excerpt":"Performance issues inside applications comes in many forms and in the best of times they can be easily identified. But they can also get you real stuck and impossible to get your h...","content":"Performance issues inside applications comes in many forms and in the best of times they can be easily identified.\nBut they can also get you real stuck and impossible to get your head around. It can be hard to know what changes to the application are needed in order for it to execute faster, in those scenarios benchmarking can be a great tool to consider.\nWe can rely on a baseline, benchmark what we have in place today, and then work from that to validate our efforts to make it faster.\n\n## Setting up benchmarks in dotnet\n\nTo setup a benchmark we start by creating a new console application and then install the nuget package `BenchmarkDotNet`.\n\nAfter that we need to specify what benchmarks we need to run. We can do this with the `BenchmarkRunner`.\n\n```csharp\npublic class Program\n{\n    public static void Main(string[] args)\n    { \n        BenchmarkRunner.Run<CustomerBenchmarks>();\n    }\n}\n```\n\nThe `CustomerBenchmark` class contains the code to benchmark. For simplicity, let say that we want to compare the performance of the linq `.First()` vs `.Singel()`. This is how that could look like:\n\n```csharp\npublic class CustomerBenchmarks\n{\n    [Params(100, 200, 300, 400, 500)]\n    public int Amount { get; set; }\n    \n    private Customer[] _customers = Array.Empty<Customer>();\n\n    [GlobalSetup]\n    public void GlobalSetup()\n    {\n        _customers = Seed(Amount);\n    }\n\n    [Benchmark]\n    public void FindFirstCustomerById()\n    {\n        _customers.First(x => x.Id == Amount/2);\n    }\n\n    [Benchmark]\n    public void FindSingleCustomerById()\n    {\n        _customers.Single(x => x.Id == Amount/2);\n    }\n\n    private static Customer[] Seed(int amount)\n    {\n        return Enumerable.Range(0, amount).Select(i => new Customer\n        {\n            Id = i,\n            Name = $\"name-{i}\"\n        }).ToArray();\n    }\n}\n```\n\n`BenchmarkDotNet` have a wide variety of attributes, I used the most fundamental ones:\n\n- `[Params]` - Populates `Amount` with different scenarios.\n- `[GlobalSetup]` - Will run for every scenario.\n- `[Benchmark]` - The method to benchmark\n\n## How to run the benchmark\n\nIn order for the benchmark to run reliable we need to build the solution with the release flag.\n\n```bash\ndotnet build --configuration release\n```\n\nAfter that we can navigate to the release folder and execute:\n\n```bash\nsudo dotnet Customer.Performance.dll\n```\n\nNote that Im using sudo, this is so that `BenchmarkDotnet` can increase the CPU priority for the process.\n\nAnd the results:\n\n![benchmark-performance-example](/images/dotnet-performance-example/benchmark-performance-example.png)\n\nAnd that pretty much it. Maybe not so surprising,  `First()` is faster then `Single()`. This is because `Single()` takes the responsibility of checking for duplicates (which is a great feature). One really nice thing about dotnet core is that we can locate the exact line at github. <https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Single.cs#L74>\n\nHappy Coding!\n"},{"slug":"dotnet-watch-in-dotnet-6","title":"dotnet watch in .NET 6","date":"2021-11-14","tags":["dotnet","productivity"],"url":"https://www.rasmusolsson.dev/posts/dotnet-watch-in-dotnet-6/","excerpt":"dotnet watch run is a command that can be very handy while developing .NET applications. Its similar to Nodemon in Node.js in that it watches for file changes and restarts the app...","content":"`dotnet watch run` is a command that can be very handy while developing .NET applications.\nIts similar to Nodemon in Node.js in that it watches for file changes and restarts the app with the new changes on file saved.\n`dotnet watch` can be quite slow though, it builds and restarts the app on save.\nThis can be quite awkward when running a huge api.\nGood news is that with the new .NET 6 feature, hot reload which is integrated into `dotnet watch`, we can make changes and apply them instantaneously without re-building the application. (There are some limitations and I will talk about them later).\n\nLet's start by looking at `dotnet watch` for .NET 5 and .NET 6.\n\n### .NET 5 with dotnet watch\n\nIn the picture below we have:\n\n- To the left: A basic web api which prints .NET version, the process Id, and the time for each request.\n- To the right: A script that invokes the web api\n- The api is started with `dotnet watch run`\n\n![dotnet-5-dotnet-watch-overview](/images/dotnet-watch-in-dotnet-6/dotnet-watch-dotnet-5-overview.png)\n\nLets edit this by capitalizing version.\n\n![dotnet-5-dotnet-watch-edit](/images/dotnet-watch-in-dotnet-6/dotnet-watch-dotnet-5-edit.png)\n\nAs we can see in the picture above, we had to rebuild the api for this change.\nThe process id also changed.\nIt took about 12 seconds for the api to be available again.\nThis time obviously varies depending on you application rebuild and startup process.\n\n### .NET 6 with dotnet watch\n\nLets do the same change for .NET 6, capitalize version.\n\n![dotnet-6-dotnet-watch-edit](/images/dotnet-watch-in-dotnet-6/dotnet-watch-dotnet-6-edit.png)\n\nAs we can see it took less then a second. Also note the `dotnet watch` output: `Hot reload of changes succeeded` and we kept the same process id.\nThats great, now we can have `dotnet watch` running constantly and save a lot of time having the api available during development. But there are some limitations.\n\n### .NET 6 dotnet watch limitation\n\nWhen executing `dotnet watch` we are prompted with the following information:\n\n![dotnet-6-dotnet-watch-limitations-prompt](/images/dotnet-watch-in-dotnet-6/dotnet-watch-dotnet-6-limitations-prompt.png)\n\nThere are situations when hot reload aren't available and we have to rebuild and restart our application (the same as we did in .NET 5). The url which is prompted contains a list of unsupported edits.\n\nIf `dotnet watch` detects that the edit is rude it will ask you the following:\n\n![dotnet-6-dotnet-watch-limitations-rude](/images/dotnet-watch-in-dotnet-6/dotnet-watch-dotnet-6-limitations-rude.png)\n\nOption A is perfect because then we can fallback to rebuild and restart the application when needed and takes benefits from hot-reload when possible. Though, it would be nice to have a non-interactive default flag for this situation.\n\nThats pretty much it. I hope you also get good use out of the new hot reload features in `dotnet watch`.\n\nHappy coding!\n"},{"slug":"github-copilot-rider","title":"Getting started with github copilot on rider","date":"2021-10-03","tags":["rider","ide","productivity"],"url":"https://www.rasmusolsson.dev/posts/github-copilot-rider/","excerpt":"About two weeks ago I got access to github copilot and I couldn't wait to explore it more in depth. To get access, you first have sign up and then wait for a licence. I signed up i...","content":"About two weeks ago I got access to github copilot and I couldn't wait to explore it more in depth.\nTo get access, you first have sign up and then wait for a licence.\nI signed up in early July so the waiting list was quite long but hopefully they release more and more licenses as the time passes.\n\nYou can sign up here: <https://github.com/features/copilot/signup>\n\nAs a fullstack developer, I use both vscode and rider ide in my daily development so I wanted it installed on both.\n\nTo install github copilot on vscode you can download the github copilot extension and then login to your github account when prompted (which is connected with github copilot licence).\n\nIn rider, I had some problem locating the plugin in plugin marketplace at first, but after retrying later I could find it. Make sure to have rider version 2021.2.* or later before install.\n\nTo install the plugin, there are two ways either you can locate it in the plugin marketplace or load the plugin from disk.\n\nInstall through plugin marketplace\n\n1. Go to file -> Settings -> Plugin\n2. Switch to marketplace and search for `github copilot`\n3. Install, restart ide and login to github when prompted\n\nInstall by loading the plugin for disk\n\n1. Download the plugin for jetbrains, it can be located here: <https://plugins.jetbrains.com/plugin/17718-github-copilot/versions>\n2. Go to file -> Settings -> Plugin\n3. Click the wheel\n4. Choose install plugin from disk and locate the zip you downloaded in step 1.\n5. Install, restart ide and login to github when prompted\n\nThat pretty much it! I hope you have a nice time working with your new pair-programmer 😊\n\nHappy coding!\n"},{"slug":"git-hook-for-dotnet","title":"Git client hooks for dotnet","date":"2021-09-12","tags":["dotnet","productivity","devops"],"url":"https://www.rasmusolsson.dev/posts/git-hook-for-dotnet/","excerpt":"I recently added some git hooks to my git hook folder for dotnet related work. Its a nice addition to my daily development which makes me more productive. Basically on every time I...","content":"I recently added some git hooks to my git hook folder for dotnet related work.\nIts a nice addition to my daily development which makes me more productive.\nBasically on every time I commit, I run the following:\n\n- dotnet build\n- dotnet test\n- dotnet format\n\nI think the choices of what to run should vary depending on the repository you are working on.\nThe problem that can occur is that if we don't run any checks locally before pushing the code, the CI server may fail. A good level might be to reflect the CI steps into your git client hooks.\n\nI like to have small but fast running hooks on each commit, aiming at having every commit green.\nBut for some scenarios like long running integration tests, I could see them fit better in pre-push hook.\n\nHere is my general pre-commit hook for dotnet repositories:\n\n```bash\n#!/bin/bash\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\n# build\necho -e \"${YELLOW}Running pre-commit hook, dotnet build...${NC}\"\ndotnet build\nrc=$?\nif [[ $rc != 0 ]] ; then\n    echo -e \"${RED}Failed to build the project, please fix this and commit again${NC}\"\n    exit $rc\nfi\n\n# test\necho -e \"${YELLOW}Running pre-commit hook, dotnet test...${NC}\"\ndotnet test --no-build\nrc=$?\nif [[ $rc != 0 ]] ; then\n    echo -e \"${RED}Test failed, please fix this and commit again${NC}\"\n    exit $rc\nfi\n\n# format\necho -e \"${YELLOW}Running pre-commit hook, dotnet format...${NC}\"\ndotnet format --no-restore --verbosity detailed\nrc=$?\nif [[ $rc != 0 ]] ; then\n    echo -e \"${RED}Failed to format project, please fix this and commit again${NC}\"\n    exit $rc\nfi\n\nexit 0\n```\n\nOne thing that could be improved is to only run these on affected files for the repository.\n\nThats pretty much it!\n\nHappy coding!\n"},{"slug":"powershell-autocomplete","title":"Powershell autocomplete with PSReadLine","date":"2021-07-30","tags":["powershell","automation","devops"],"url":"https://www.rasmusolsson.dev/posts/powershell-autocomplete/","excerpt":"As of powershell 6 you have a module called PSReadLine installed by default. This module changes the editing experience of powershell and is customizable. One thing that I really l...","content":"As of powershell 6 you have a module called PSReadLine installed by default. This module changes the editing experience of powershell and is customizable. One thing that I really like is to have auto completion for historical commands. In this post I will show you how I setup PSReadLine in my profile.  \n\nStart by installing PSReadLine.\n\n```powershell\nGet-Module -ListAvailable PSReadLine\nInstall-Module PSReadLine\n```\n\nThen open up profile `code $PROFILE`\n\n```powershell\n\n#Imports PSReadLine\nImport-Module PSReadLine\n\n#Tab - Gives a menu of suggestions\nSet-PSReadLineKeyHandler -Key Tab -Function MenuComplete\n\n#UpArrow will show the most recent command\nSet-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward\n\n#DownArrow will show the least recent command\nSet-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward\n\n#During auto completion, pressing arrow key up or down will move the cursor to the end of the completion\nSet-PSReadLineOption -HistorySearchCursorMovesToEnd\n\n#Shows tooltip during completion\nSet-PSReadLineOption -ShowToolTips\n\n#Gives completions/suggestions from historical commands\nSet-PSReadLineOption -PredictionSource History\n\n```\n\n![powershell-psreadline-autocompletion](/images/powershell-autocomplete/powershell-psreadline-tab.png)\n\n![powershell-psreadline-autocompletion](/images/powershell-autocomplete/powershell-autocomplete.png)\n\nMore about PSReadLine can be found here: <https://github.com/PowerShell/PSReadLine>\n\nHappy coding!\n"},{"slug":"increasing-typescript-type-safety","title":"Increasing TypeScript Type-Safety","date":"2021-06-30","tags":["typescript","devops"],"url":"https://www.rasmusolsson.dev/posts/increasing-typescript-type-safety/","excerpt":"Even though TypeScript have type checking and type safety you can opt out by using the type \"any\" for a variable or function. This \"any\" type have both upsides and downsides as des...","content":"\nEven though TypeScript have type-checking and type-safety you can opt out by using the type \"any\" for a variable or function.\nThis \"any\" type have both upsides and downsides as described in the TypeScript documentation.\nIt can for example give value when migrating from JavaScript to typescript and you want to do this gradually.\n\n> The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.\n\nBut it can also get abused.\nTypeScript types can get very complex and it can be an easy escape route to just provide type \"any\" and move on with your code. Another example is third-party libraries that doesn't come with type definitions. Here you can write your own type definitions by proving a *.d.ts file but its also another easy escape route by using it as type \"any\".\n\nQuoting from the TypeScript documentation:\n> After all, remember that all the convenience of any comes at the cost of losing type safety. Type safety is one of the main motivations for using TypeScript and you should try to avoid using any when not necessary.\n\nSo how do we as software developers make sure that we don't fall into the pit hole of overrepresented our code with type \"any\".\n\nThere are some common ways we can get rid of the type \"any\" completely. We can enable the ts compiler option: `noImplicitAny` together with an eslint rule `no-explicit-any`.\n\nBut how about the scenario where you can't just activate it because its just to much effort right now?\n\nLet's say the team agreed on disallowing type \"any\" in the future and by using boy scouting eventually will get there or is currently migrating to TypeScript from JavaScript. How do we measure the progress towards removing it completely?\n\nOne tool that help is type-coverage.\n\n> A CLI tool to check type coverage for typescript code\n\nWe can install it to our dev dependencies and expose it to our scripts in package.json.\n\n```bash\nnpm install --save-dev type-coverage\n```\n\n```json\n  \"scripts\": {\n    ...\n    \"ts-coverage\": \"type-coverage\"\n    ...\n  },\n```\n\nPerform `npm run ts-coverage` will give us the total coverage for example: 539 / 557 96.76%\n\nTo make sure we are not reducing our type-coverage we can add the `--at-least` argument.\n\n```json\n  \"scripts\": {\n    ...\n    \"ts-coverage\": \"type-coverage -at-least 99\"\n    ...\n  },\n```\n\nNow we can just add this as CI step and we can block the build from continue.\n\nAnother great tool is the typescript-coverage-report. It will give us a more details in a form of table and show us files missing coverage.\n\nIn example:\n\n![typescript-coverage-report](/images/increasing-typescript-type-safety/typescript-coverage-report.png)\n\nIt includes the `ts-coverage` module so no need to install both.\n\n```json\n  \"scripts\": {\n    ...\n    \"ts-coverage\": \"type-coverage-report --threshold 100\"\n    ...\n  },\n```\n\nThats it! Now we can measure our progress removing the type \"any\" and make sure we progress in the right direction.\n\nReferences:\n\n- noExplicitAny:<https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md>\n- noImplicitAny: <https://www.typescriptlang.org/tsconfig>\n- type-coverage: <https://github.com/plantain-00/type-coverage>\n- typescript-coverage-report: <https://github.com/alexcanessa/typescript-coverage-report>\n\nHappy Coding!\n"},{"slug":"creating-dotnet-templates","title":"Creating a dotnet template project","date":"2021-05-31","tags":["dotnet"],"url":"https://www.rasmusolsson.dev/posts/creating-dotnet-templates/","excerpt":"This post has been updated and is now available at: Happy Coding!","content":"\nThis post has been updated and is now available at: <https://rasmusolsson.dev/posts/creating-a-net-template-with-template-json>\n\nHappy Coding!\n"},{"slug":"visualizing-project-dependencies","title":"Visualizing dependencies in React","date":"2021-04-21","tags":["react"],"url":"https://www.rasmusolsson.dev/posts/visualizing-project-dependencies/","excerpt":"At some point in time, it might become handy to visualize your dependencies. Maybe you have a refactoring initiative and want an easy way to display or grasp how things look. In th...","content":"At some point in time, it might become handy to visualize your dependencies.\nMaybe you have a refactoring initiative and want an easy way to display or grasp how things look. In this post, we will take a look at some tools that might be helpful in that.\n\n## Dependency cruiser\n\nProbably the most popular tool to visualize dependencies for React.\nIt can combine a set of rules, visualize and validate the different dependencies.\n\n```bash\nnpm install --save-dev dependency-cruiser\n```\n\nTo visualize the structure as a picture we can pipe the result to graphviz.\n\nYou can install graphviz at <https://www.graphviz.org/> or with chocolatey or most other package managers:\n\n```bash\nchoco install graphviz\n```\n\nAdd to package.json:\n\n```json\n\"scripts\": {\n    \"architecture\": \"depcruise --include-only \\\"^src\\\" --output-type dot src | dot -T svg > dependencygraph.svg\"\n}\n```\n\nRun:\n\n```bash\nyarn architecture\n```\n\nRunning this on my redux-toolkit-typescript-example app would result in:\n\n![dependencygraph](/images/dependencygraph.png)\n\n## React sight\n\nReact sight is a small extension that can be used to visualize components and dom elements as a graph. Its can be quite buggy and works best running on a local setup during development. Note that it also can be quite cpu heavy on big applications.\n\n1. Install react sight plugin\n2. Install react developer tools\n3. Install redux DevTools\n4. Inspect element and you should be able to see react sight in the tabs to the right. Clicking it will start a process of building the graphs, components and dom elements.\n\nExample:\n\n![react-sight](/images/react-sight.png)\n\n## Madge\n\nMadge is another tool that can help visualize dependencies. It has easy to use cli that can be combined with different flags such as catching circular dependencies.  \n\n```bash\nnpm install --save-dev madge\n```\n\nTo visualize the structure as a picture we need graphviz.\n\nYou can install graphviz at <https://www.graphviz.org/> or with chocolatey or most other package managers.\n\n```bash\nchoco install graphviz\n```\n\nAdd to package.json:\n\n```json\n\"scripts\": {\n    \"architecture\": \"madge --extensions ts,tsx --image graph.svg src/\"\n}\n```\n\nNote the `--extensions` flag. By default it includes `.js` files.\n\nRunning this on my redux-toolkit-typescript-example app would result in:\n\n![madge](/images/madge.png)\n\n## Conclusion\n\nVisualizing dependencies can be a great tool in some cases and it requires quite a low effort to set up. I find it most useful when discussing new conventions, planning larger features or simply obtaining a reality check on how the actual structure look.\n\nHappy Coding!\n"},{"slug":"css-modules-vs-global-css","title":"CSS Modules vs Global CSS","date":"2021-03-22","tags":["css-modules","css"],"url":"https://www.rasmusolsson.dev/posts/css-modules-vs-global-css/","excerpt":"There are many ways to style a website with css. In this post I will share my findings migrating from global css to css modules. Before we start comparing, lets have a quick look a...","content":"There are many ways to style a website with css. In this post I will share my findings migrating from global css to css modules. Before we start comparing, lets have a quick look at what global and css modules are.\n\n## Global CSS\n\nGlobal css is the most widely used form for styling websites.\nWe add a css file, we include it on the site and its content is available to use on any html element.\n\n## CSS modules\n\nAka component-scoped css. The css we include is scoped to the module referring the css file.\nTo use css modules we need to do a few things.  \n\n### Configure CSS loader\n\nThis is already done behind the scenes for both gatsby and create-react-app (CRA).\n\nA webpack example:\n\n```javascript\nmodule: {\n  rules: [\n    {\n      test: /\\.css$/,\n      use: [\n        'style-loader',\n        {\n          loader: 'css-loader',\n          options: {\n            importLoaders: 1,\n            modules: true\n          }\n        }\n      ],\n      include: /\\.module\\.css$/\n    },\n    {\n      test: /\\.css$/,\n      use: [\n        'style-loader',\n        'css-loader'\n      ],\n      exclude: /\\.module\\.css$/\n    }\n  ]\n}\n```\n\n### Renaming the CSS file\n\nIn order for the css loader to distinguee pure css files from css modules we need to rename the file to include \"module\", in example: `layout.css` to `layout.module.css`.\n\n## Implementation details\n\nWhen working with global css in react, all we need to do is to include it like this:\n\n```javascript\nimport './article-overview.css'\n```\n\nWe then add the class to the element:\n\n```jsx\n<div className='article-overview-container'>\n```\n\nIn css modules we work with objects like any object in javascript:\n\n```javascript\nimport styles from './article-overview.module.css' \n```\n\n```jsx\n<div className={styles.articleOverviewContainer}>\n```\n\nAnd.. we can even use named imports:\n\n```javascript\nimport {classname1, classname2 } from './article-overview.module.css' \n```\n\nWhich would give us warnings or errors(configurable) when for example misspelling an import after a rename:\n\n![esmodule-css-module-build-warning](/images/css-modules/esmodule-css-module-build-warning.png)\n\n## Naming\n\nThe rendered css content might be more clear when using global css depending on how you name your css classes.\nWhen using global css I like to prefix classes with the filename to make sure it's unique. In example:\n\n```css\n.article-overview-container {\n  border-bottom: 1px solid #f1f1f1;\n  margin-bottom: 30px;\n  padding-bottom: 30px;\n}\n```\n\n`article-overview` is the prefix and `container` is the intended name.\n\nWith css-modules a prefix and suffix is automatically added to the css classnames.\n\n```css\n.container {\n  border-bottom: 1px solid #f1f1f1;\n  margin-bottom: 30px;\n  padding-bottom: 30px;\n}\n```\n\n![css-module-scooping-2](/images/css-modules/css-module-scoping-2.png)\n`Red = filename + module. blue = classname, teal = hash.`\n\nNote that this is the default naming convention for gatsby. There are other ways to set up your naming conventions by configuring the css-loader.\nSee `localIdentName` at <https://webpack.js.org/loaders/css-loader/>\n\nMy convention in this example is: `[filename]-module--[classname]--[hash]`\n\n## Conclusion\n\nCss modules give us a way to ensure that a css file is unique across components and not accidentally accessed elsewhere. This can be especially useful when working in bigger solution across many different teams. It can also gives us type-safety when using named imports to reduce the chance of importing a class that isn't defined.\nIt can though be a bit harder to find the actual css file from a classname. Nonetheless, knowing the naming convention would help a lot to resolve that.\n\nHappy Coding!\n"},{"slug":"wordpress-to-gatsby","title":"Moving from wordpress to gatsby","date":"2021-03-14","tags":["gatsby","react","graphql","css"],"url":"https://www.rasmusolsson.dev/posts/wordpress-to-gatsby/","excerpt":"I few weeks ago I decided to move on from wordpress and build my own site. Wordpress have served me well for the past 2 years but as a developer it has always felt a bit to much bl...","content":"\nI few weeks ago I decided to move on from wordpress and build my own site.\nWordpress have served me well for the past 2 years but as a developer it has always felt a bit to much black-box.\n\nIn this post I will go through my new setup with gatsby.\n\n## Gatsby\n\nGatsby is a framework that is particularly used to build static pre-compiled websites.\nIt is built on top of react and you can extend it to render dynamic pages like any react application.\nIt:\n\n- has flexible graphql api with graphiql (a helpful interface) to query and mutate data\n- offers a rich amount of plugins that can be configured\n- supports both Typescript(@types) and Javascript\n- describe posts through .md or .mdx files. Regular markdown files(.md) and markdown combined with custom react components(.mdx) for more customization.\n\n## Hosting the site\n\nI went with Netlify.\nNetlify has great supports for gatsby, pre-configured build templates, a generous free tier, let's encrypt(free SSL), dns, github integration and easy to configure CI/CD.\n\n![my-netlify-overview](/images/netlify-overview.png)\n\n## Daily usage\n\nWhen I write a post with gatsby I write it as a .md file in vscode. After that I push the code to github. This then notified netlify which starts the CI/CD builds. The artifacts are then uploaded to the new site with the added post.\n\nThis means that I need to redeploy the site in order to add a post. I don't use any database, and the deploy only takes a few minutes so I think that is a good trade of(to not pay for a database).\nThere are options to integrate gatsby with data sources. You can then store the .md files in a database and use the graphQL API to query it.\n\nMore about different data sources can be found here: <https://www.gatsbyjs.com/docs/tutorial/part-four/>\n\n## Site speed\n\nThere are many benefits of using gatsby for good site speed. It's pre-compiling the site into static pages and a lot of the computation is made on compile time. \nGatsby also benefits from SPA and only re-renders parts of the application on route changes.\n\n## Costs\n\n- Netlify free tier\n- Let's encrypt (free SSL)\n- Domain name (still using Godaddy from my previous wordpress setup)\n\nWhen down from about 150-200$ to around 10£ a year.\n\n## Gatsby plugins\n\nAt the time of writing, I use a the following gatsby plugins:\n\n```javascript\ngatsby-plugin-typescript //typescript support\ngatsby-plugin-sharp //gatsby-transformer-sharp for loading .md files and images\ngatsby-plugin-react-helmet //to manipulate the <head> tag in html (mostly) for SEO\ngatsby-plugin-feed //RSS feed\ngatsby-plugin-manifest //manifest file, PWA, add to home screen and logo's\ngatsby-plugin-offline //offline mode, pre-caching\ngatsby-plugin-sitemap //sitemap\ngatsby-plugin-google-gtag //google analytics\n```\n\n## Conclusion\n\nGatsby is a great choice if you know a bit of frontend web-development. Its easy to get started and setup and offers a wide range of plugins. Its very fast, backed by a big community and built on top of react.\n\nMore about gatsby:\n\n- Home <https://www.gatsbyjs.com>\n- Tutorial: <https://www.gatsbyjs.com/docs/tutorial/>\n- Course: <https://www.udemy.com/course/gatsby-tutorial-and-projects-course/>\n\nHappy Coding!\n"},{"slug":"productive-with-the-terminal","title":"Productive with the Terminal","date":"2021-02-07","tags":["powershell","productivity","devops"],"url":"https://www.rasmusolsson.dev/posts/productive-with-the-terminal/","excerpt":"When I’m working on a long running project, It’s not unusual to be working in a specific area of the code base for a while. Being productive with the terminal and take time to set...","content":"\nWhen I’m working on a long-running project, It’s not unusual to be working in a specific area of the code base for a while. Being productive with the terminal and take time to set up terminal automation may save a lot of time in the long run.\n\nIn this post I will show some of my techniques I do to be more productive. I will be using PowerShell on Windows Terminal in this example.\n\nImagine we are focusing the upcoming months in an area involving the following:\n\n- A MSSQL database\n- An .NET Core API\n- A React SPA frontend\n- A normal morning would include:\n\nA normal morning could like something like:\n\n1. Turn on computer.\n1. Login\n1. Start PowerShell\n1. Cd to git repositories for what I’m working on\n1. Git fetch/pull/rebase to make sure I have the latest code\n1. Start Build + Start IDE’s + Run\n1. Open up available MR’s/PR’s\n1. Open up the issue management system for example JIRA\n1. Open up the chat\n\nThat’s quite a lot of manual steps.\n\nLet’s look at how we can automate that.\n\nIn PowerShell, you can add global functions to your profile.\n\n1. Open up PowerShell and edit $PROFILE, for example code $PROFILE\n2. Add a function called Good-Morning\n\n```powershell\nfunction Good-Morning {\n}\n```\n\n3. Step, 3-6 (cd repo, get the latest code, build code, start IDE, run code)\n\n```powershell\nfunction Good-Morning {\n    Morning-Frontend\n    Morning-Backend\n    Morning-Database\n};\nfunction Morning-Frontend {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-frontend-project \\;\n        git pull \\;\n        npm install \\;\n        code . \\;\n        npm start \\;\n    }\"\n}\nfunction Morning-Backend {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-backend-project \\;\n        git pull \\;\n        dotnet build \\;\n        start your-solution-file \\;\n        dotnet watch run your-cs-proj\n    }\"\n}\nfunction Morning-Database {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-database-project \\;\n        git pull \\;\n        docker build -t database . && docker run -it database\n       }\"\n}\n```\n\nOk great! Now that we have that out of the way, the rest of the steps will be easy.\n\nStep 7 and 8. Accessing MR/PR’s on github.com and JIRA with chrome.\n\n```powershell\nchrome.exe \"https://github.com\" \"https://jira.com\"\n```\n\nStep 9. Accessing Chat, lets say we use Slack. We bind and access the environment variable.\n\n```powershell\nslack\n```\n\nThat pretty much it, now we have the following morning routine:\n\n1. Turn on computer.\n2. Login\n3. Start PowerShell\n4. Good-Morning\n\nAnd when we feel comfortable, we can add it to start-up resulting in:\n\n1. Turn on computer.\n2. Login\n\nFull script below:\n\n```powershell\nfunction Good-Morning {\n    Morning-Frontend\n    Morning-Backend\n    Morning-Database\n    chrome.exe \"https://github.com\" \"https://jira.com\"\n    slack\n};\n\n# New Terminal, latest code, build npm, open code, run\nfunction Morning-Frontend {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-database-project \\;\n        git pull \\;\n        npm install \\;\n        code . \\;\n        npm start \\;\n    }\"\n}\n\n# New Terminal, latest code, build dotnet, open rider, run\nfunction Morning-Backend {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-database-project \\;\n        git pull \\;\n        dotnet build \\;\n        start your-solution-file \\;\n        dotnet watch run your-cs-proj\n    }\"\n}\n\n# New Terminal, latest code, build dockerfile and run\nfunction Morning-Database {\n    wt.exe PowerShell.exe, \"-NoExit\", \"-Command\",\n    \"& {\n        Push-Location C:\\git-database-project \\;\n        git pull \\;\n        docker build -t database . && docker run -it database\n       }\"\n}\n```\n\nHappy Coding!\n"},{"slug":"productive-with-git-aliases","title":"Productive with git aliases","date":"2021-01-31","tags":["git","productivity","devops"],"url":"https://www.rasmusolsson.dev/posts/productive-with-git-aliases/","excerpt":"When I started working as a developer I didn’t actually start with git but with TFS. Nowadays, everywhere I go git is standard and based on google trends (picture below) and my exp...","content":"\nWhen I started working as a developer I didn’t actually start with git but with TFS. Nowadays, everywhere I go git is standard and based on google trends (picture below) and my experience with both of them I will be doubtful that will change anytime soon.\n\n![git-vs-tfs-vs-subversion](/images/git-vs-tfs-vs-subversion-1.webp)\n\nIn the past weeks, I’ve invested some extra time in revisiting my git aliases and this is my result:\n\n```bash\n[alias]\n         a  = add\n        ac = !\"git a . && git c -m\"\n        amend = !\"git c --amend\"\n        oops = !\"git commit --amend --no-edit\"\n        oopsf = commit -a --fixup\n        ap = add --patch\n        back = !\"git reset HEAD^1\"\n        b = branch\n        ba = \"!git for-each-ref --sort='-authordate' --format='%(authordate)%09%(objectname:short)%09%(refname)' refs/heads | sed -e 's-refs/heads/--'\"\n        c = commit\n        cam = !\"git c -a -m\"\n        cm = !\"git c -m\"\n        co = checkout\n        conf = !\"git config --global --edit\"\n        cb = \"!git ba | git grep {$1}\"\n        cob = checkout -b\n        create = \"!bt() { git co -b BT-$1-$2; git push -u -o merge_request.create -o merge_request.remove_source_branch -o merge_request.title=\\\"Draft: BT-$1 $3\\\"; }; bt\"\n        navigate = !git add . && git commit -m 'WIP-mob' --allow-empty --no-verify && git push -u --no-verify\n        drive = !git pull --rebase && git log -1 --stat && git reset HEAD^ && git push --force-with-lease\n        cleanit = !\"g co . && g clean -df\"\n        d = diff\n        dt = difftool --dir-diff\n        f = fetch\n        last = log -1 HEAD --stat\n        lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all\n        la = \"!git config -l | grep alias | cut -c 7-\"\n        p = push -u\n        pu = pull --rebase\n        r = reset\n        rl = reflog\n        rs = reset --soft\n        rsl = reset --soft HEAD~1\n        rh = reset --hard\n        rhl = reset --hard HEAD~1\n        rb = rebase --interactive\n        rbas = rebase --autosquash --interactive \n        rba = rebase --abort\n        rbc = rebase --continue\n        rbs = rebase --skip\n        please = push --force-with-lease\n        t = !\"git lg1\"\n        st = status\n```\n\nMany of them are self-explanatory but some might need some explanations.\n\nIf you noticed closely, I didn’t have any git merge aliases. I have been dropping them out going forward. From 1 of Jan 2021, I’ll go with git rebase completely due to cleaner git history and to have more control over what’s going on.\n\nWhen I’m playing around locally, I sometimes want a simple restart button.\n\n```bash\ncleanit = !\"g co . && g clean -df\"\n```\n\n`git ba` Will list all branches order by last modified\n\n```bash\nba = \"!git for-each-ref --sort='-authordate' --format='%(authordate)%09%(objectname:short)%09%(refname)' refs/heads | sed -e 's-refs/heads/--'\"\n```\n\nThe `git lg1/lg2` is just some nice git log formats that I found on stackoverflow.\n\n```bash\nlg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all\n\nlg2 = log --graph --abbrev-commit --decorate --format=format:\"%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'\"\n```\n\nThe `git la` will list all git aliases.\n\n```\nla = \"!git config -l | grep alias | cut -c 7-\"\n```\n\nLastly, I have bind `g = git`.\nIn bashrc:\n\n```bash\n#g for git\nalias g='git'\nsource /usr/share/bash-completion/completions/git\ncomplete -o default -o nospace -F _git g\n```\n\nIn Powershell, open `$PROFILE`\n\n```powershell\nSet-Alias g git\n```\n\nHappy coding!\n"},{"slug":"advent-of-code-2020-typescript-deno","title":"Advent of Code 2020 – TypeScript & Deno","date":"2020-12-25","tags":["aoc","deno","typescript"],"url":"https://www.rasmusolsson.dev/posts/advent-of-code-2020-typescript-deno/","excerpt":"I’ve been quite busy the past month attending Advent of Code 2020 and spent a lot of time to do the assignments. Advent of code is a puzzle game that occurs during advent, from Dec...","content":"\nI’ve been quite busy the past month attending `Advent of Code 2020` and spent a lot of time to do the assignments.\n\nAdvent of code is a puzzle game that occurs during advent, from December 01-25 and each day has two puzzles.\n\nI decided to go with TypeScript as the programming language on the Deno platform. For the past 6 months, my main programming language at work has been TypeScript and I really like it so far.\n\nWhy deno then..?\n\nTypeScript is a first-class citizen on Deno. It’s made with Rust which makes it fast and secure but on the downside, it doesn’t have a lot of libraries and utilities(yet..) compared to Node.\n\nThe biggest advantage I had doing deno was that you simply do `deno run --allow-read index.ts` to run the TypeScript file directly without having to compile it into js first. You can do this on Node as well by doing for example `tsc index.ts | node index.js` . But deno has the compiler built-in and linting was provided out of the box which in my opinion made deno a better fit.\n\nAll in all, I think `Advent of Code 2020` was very fun and I will definitely attend it again in 2021. I provide some links below if you are interested to have a closer look at the code or the event itself.\n\nAdvent of Code: https://adventofcode.com/2020/\n\nGithub: https://github.com/raholsn/advent-of-code\n\nHappy Coding!\n"},{"slug":"redux-toolkit-typescript-example","title":"Redux Toolkit Typescript Example","date":"2020-11-30","tags":["redux","react"],"url":"https://www.rasmusolsson.dev/posts/redux-toolkit-typescript-example/","excerpt":"In my previous post, I wrote about the new standard of using Redux Toolkit and the beauty it gives on integrating the Redux guidelines into a framework. In Redux Toolkit the last s...","content":"\nIn my previous post, I wrote about the new standard of using Redux Toolkit and the beauty it gives on integrating the Redux guidelines into a framework.\n\nIn Redux Toolkit the last section, advanced tutorials, you can find how to use Redux Toolkit with TypeScript.\n\nhttps://redux-toolkit.js.org/tutorials/advanced-tutorial\n\nIt’s a comprehensive tutorial and covers a lot of details so I thought I could share my experience through a GitHub repository example which puts a lot of pieces together in a basic working application.\n\nFeel free to have a look and if you have any ideas, please let me know in a pull request.\n\nhttps://github.com/raholsn/redux-toolkit-typescript-example\n\nHappy coding!\n"},{"slug":"redux-toolkit-the-new-standard","title":"Redux toolkit the new standard","date":"2020-10-14","tags":["redux","react"],"url":"https://www.rasmusolsson.dev/posts/redux-toolkit-the-new-standard/","excerpt":"As a developer, we regularly read and learn about new frameworks. We read the API documentation and start implementing. After that or in the best case before we even start, we disc...","content":"\nAs a developer, we regularly read and learn about new frameworks.\nWe read the API documentation and start implementing. After that or in the best case before we even start, we discuss the implementation plan with the team. Some questions that maybe be raised:\n\n- Where should we put the logic\n- Should we normalize the data\n- What naming convention should we use\n- Should we use tabs or spaces…?\n\nIf you’ve done Redux before you might have come across the Redux style guide. The style guide is a set of best practices and conversions on how to use Redux. It helps us as developers to come to a convention that is opinionated by the developer of Redux. It helps us come together, clear out unknowns and liking, and produce good code faster.\n\nRedux toolkit is such a guideline, even if it’s not directly part of the “Redux” style guide documentation, it’s still something that the creator of Redux opinionated.\n\n> Redux Toolkit our official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.\n>\n> - https://redux.js.org/redux-toolkit/overview\n\nThe Redux Toolkit contains utilities and defaults that we usually use in our setup of Redux. For example, including Redux DevTools during store setup and redux thunk out of the box.\n\nIt also comes with great utility functions such as CreateSlice which helps export reducer and action creators with implicit type names which reducing the boiler-plating that Redux can be famous for.\n\nThe Redux Toolkit is designed with openness and flexibility in mind and can be customized in various ways to handle advanced scenarios while still proving default and best practices of Redux, directly inside the framework itself.\n\nIf you haven’t checked out The Redux Toolkit, I highly recommend going through the tutorial, link provided below.\n\nhttps://redux-toolkit.js.org/tutorials/basic-tutorial\n\nHappy coding!\n"},{"slug":"why-docker-dotnet-changes-the-way-we-test-our-applications","title":"Why Docker.DotNet changes the way we test our applications","date":"2020-09-27","tags":["dotnet","docker","devops"],"url":"https://www.rasmusolsson.dev/posts/why-docker-dotnet-changes-the-way-we-test-our-applications/","excerpt":"Docker.Dotnet is a library that you can use to interact with the docker host Remote API. It’s a great library to use if we want to dynamically create containers that need to be con...","content":"\nDocker.Dotnet is a library that you can use to interact with the docker host Remote API. It’s a great library to use if we want to dynamically create containers that need to be controlled within the application.\nSuch as spinning up a local database, RabbitMQ cluster, or an API, while for example, doing integration- and performance-testing.\n\nIf you haven’t heard about Docker.Dotnet, great and don’t worry, I will go through it in this post.\n\nYou might be familiar with using the Docker CLI to pull down and create images on which you start and create container with.\n\nFor example, we can use it to spin up a local Postgres database that our application connects to. We simply open up our terminal and enter this command:\n\n```bash\ndocker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres\n```\n\nThis works very well while we locally test out applications, as they do not interfere with any shared database such as the databases in the dev environment. We may keep it running after the application restarts- it will just be there when we need it.\n\nThings get a bit more tricky when we want it to happen more dynamically…\n\nLet’s say we want to set up the database precisely before we start our integration- or performance-tests. We will have to manually start it before we run and also remove it when the tests are completed, to make sure it’s in a fresh state for each run. Maybe we can have a custom pre-start build event in our csproj that will setup the database through a command line. But that will also decouple the application tests with the database, which will make it harder to troubleshoot and maintain, especially in the CI environment.\n\nIf the integration- and performance-test could have full control over its external dependencies and replace them with containers, we would have a perfect environment that does not interfere with anyone, is immutable, and fully customizable for our test scenarios. This is where Docker.Dotnet can help us.\n\n## Setting up Docker.Dotnet\n\nAs I want to use this in particular for integration- and performance-testing, my idea is to create a library called Docker.Library that my test project can refer to for spinning up external dependencies.\n\nFirst, we need to download the NuGet package.\n\n```\nInstall-Package Docker.DotNet -Version 3.125.4\n```\n\nWe then create a new class, I call it `DockerHost.cs`\n\n```csharp\npublic class DockerHost : IAsyncDisposable\n   {\n       private readonly DockerClient _dockerClient;\n       public DockerHost()\n       {\n           _dockerClient = new DockerClientConfiguration(new Uri(DockerApiUri())).CreateClient();\n       }\n   }\n```\n\nThe DockerClientConfiguration takes a Uri which should point to the docker socket. This varies based on the operation system. I extracted it as a private method.\n\n```csharp\nprivate static string DockerApiUri()\n     {\n         var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);\n\n         if (isWindows)\n         {\n             return \"npipe://./pipe/docker_engine\";\n         }\n\n         var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);\n\n         if (isLinux)\n         {\n             return \"unix:///var/run/docker.sock\";\n         }\n\n         throw new Exception(\n             \"Default socket location was not found. Please review your docker socket location\");\n     }\n```\n\nNext thing is to start interacting with docker. We need to be able to pull an image if it does not exist.\n\n```csharp\nprivate async Task PullImageIfNotExist(string image, CancellationToken ct = default)\n       {\n           var existingContainers = await _dockerClient.Containers.ListContainersAsync(new ContainersListParameters\n           {\n               All = true\n           }, ct);\n\n           var exists = existingContainers.Any(x => x.Image == image);\n\n           if (!exists)\n           {\n               await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters\n               {\n                   FromImage = image,\n               }, null, null, ct);\n           }\n       }\n```\n\nThis will list all images and check if the image exists. If not, it will download and create the new image. The default download location is from docker hub. If you want to use a custom docker repository location, you can provide it as one of the CreateImageAsync parameters.\n\nWe now need to be able to create a container from the image.\n\n```csharp\nprivate async Task<string> CreateContainer(string image,string containerName, CancellationToken ct = default)\n        {\n            await PullImageIfNotExist(image, ct);\n\n             return (await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters\n            {\n                Image = image,\n                Name = containerName,\n                HostConfig = new HostConfig\n                {\n                    PublishAllPorts = true,\n                    AutoRemove = true\n                }\n            }, ct)).ID;\n        }\n```\n\nAnd finally we need to start the Container which we created.\n\n```csharp\npublic async Task StartContainer(string image,string containerName, CancellationToken ct = default)\n       {\n           var containerId = await CreateContainer(image,containerName,ct);\n           await _dockerClient.Containers.StartContainerAsync(containerId, new ContainerStartParameters(), ct);\n       }\n```\n\nUsing the Docker.Library would look like:\n\n```csharp\nclass Program\n    {\n        static async Task Main(string[] args)\n        {\n\n            try\n            {\n                var dockerHost = new DockerHost();\n                await dockerHost.StartContainer(\"postgres\", $\"integrationtest-{Guid.NewGuid()}\");\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(e);\n                throw;\n            }\n\n        }\n    }\n```\n\nThis will start a Postgres container.\n\nThat concludes setting up a Docker.Library.\n\nThe source: https://gist.github.com/raholsn/8ab21851341d91f9bd0c7af947765404\n\nDocker.Dotnet: https://github.com/dotnet/Docker.DotNet\n\nIf you would like to take it further by integrating it with your integration-tests. I would recommend using it in conjunction with Microsoft.AspNetCore.Mvc.Testing.\n\nhttps://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1\n\nHappy Coding!\n"},{"slug":"pop_os-system-76","title":"My Pop_OS! & System 76 Laptop","date":"2020-08-27","tags":["geekout","linux","pop_os!"],"url":"https://www.rasmusolsson.dev/posts/pop_os-system-76/","excerpt":"A few months ago I was looking for a new laptop. My demands were quite high… I wanted a super light, easy to carry around laptop that I can do programming on. I also wanted it to h...","content":"\nA few months ago I was looking for a new laptop. My demands were quite high… I wanted a super light, easy to carry around laptop that I can do programming on. I also wanted it to have a pre-installed and hardware adapted Linux distro which could be my primary programming laptop for ad hoc work.\n\nAfter looking around, I come across System76 and my choice was to go with a Lemur Pro.\n\nWhen you buy a laptop from System76 you have to choice to go with either Ubuntu or Pop_OS!. Pop_OS! is basically a fork from Ubuntu which is developed and maintained by System76. The distribution is free to use on any computer but primary adapted to their own made computers. It features a custom gnome desktop and offers out of the box support for both AMD and Nvidia GPUs.\n\n![my-pop-os-desktop](/images/my-pop-os-desktop.png)\n\nMy gnome desktop. Downloading the Gnome Tweaks really lets you customize anything you want. It can be found in the Pop!\\_Shop, which in general, is the easiest way to install and maintain applications on Pop_OS.\n\n![my-pop-os-desktop](/images/pop-os-sensors.png)\n\nThe lemur pro can get quite warm. It runs around 60 C while just browsing the web. It’s on the other hand very quiet – it’s make you wonder if you even have fans running. The problem is that the fans output are located in the gap between the laptop display, which make me feels that the hot air have a hard time escaping.\n\n![my-pop-os-desktop](/images/lemur-pro-keyboard-layout.png)\n\nOne thing that may be a downfall for most people is that the Lemur pro can’t be order with other keyboard layouts than the EN layout.\nSo writing an arrow function (=>) maybe be hard if you don’t have the ambition to learn EN key layout or plan to use an external keyboard.\n\nI’m in general very satisfied with my Lemur Pro laptop. I think it has some great benefits over my previous Windows 10 laptop. Taking advantage of the fast file system that Linux offers(no time grabbing a coffee while waiting for NPM install 🙂 ). The Pop!\\_store really takes care of the boiler plating app installations that you may be exposed to running Linux.\n\nIf you need a laptop that is easy to carry around while ad hoc, Lemur Pro could be a good choice. I would recommend though, grabbing a bit juicier variant, for example Oryx pro if you’re planning on having it more stationary.\n\nhttps://system76.com/\n\nhttps://github.com/pop-os\n\nhttps://github.com/pop-os/shop\n"},{"slug":"quality-matters-setting-up-sonarqube","title":"Quality matters – Setting up Sonarqube","date":"2020-07-17","tags":["dotnet","sonarqube","devops"],"url":"https://www.rasmusolsson.dev/posts/quality-matters-setting-up-sonarqube/","excerpt":"As a developer you regularly find bad code. The definition of bad code is a very broad topic and many books have been written to cover different techniques to write better code… an...","content":"\nAs a developer you regularly find bad code. The definition of bad code is a very broad topic and many books have been written to cover different techniques to write better code… and reading them is great for learning how to do that.\n\nThe main problem I’ve experience when reading about these techniques is that in practice the single outstanding argument for not writing good code is not that you don’t know how to do it, but rather that you don’t take or have the time to do it.\n\nYou may have a hard deadline, that if you don’t deploy this on Monday morning your business will lose money. That’s a valid argument. So you, the product manager or scrum master is pointing out to create a task in the backlog to clean up that technical dept after the release. This is also a great approach! We have now fulfilled the business needs and the technical dept. But what about those tech debts that you as a developer may have missed or the team conventions that you didn’t follow or that OWASP top 10 issue that you didn’t know about? How can we minimize the risk of them occurring without spending time during a rushed project?\n\nOne tool that can help here is Sonarqube.\n\nSonarqube is a continuous code inspection tool. Whose idea is to statically analyze the source code to prompt for hints, quality improvements, conventions and potential bugs. You can and have to configure Sonarqube to meet your teams expectations and convention. Sonarqube supports many languages and is easy to get continuous by integrating it with your CI pipelines.\n\nTo get started, we need to set up the sonarqube server.\nThe docker-compose file below will set up sonarqube together with the dependent PostgreSQL database.\n\n```yml\nversion: '3'\n\nservices:\n  sonarqube:\n    image: sonarqube\n    expose:\n      - 9000\n    ports:\n      - '127.0.0.1:9000:9000'\n    networks:\n      - sonarnet\n    environment:\n      - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/sonar\n      - SONARQUBE_JDBC_USERNAME=sonar\n      - SONARQUBE_JDBC_PASSWORD=sonar\n    volumes:\n      - sonarqube_conf:/opt/sonarqube/conf\n      - sonarqube_data:/opt/sonarqube/data\n      - sonarqube_extensions:/opt/sonarqube/extensions\n      - sonarqube_bundled-plugins:/opt/sonarqube/lib/bundled-plugins\n\n  db:\n    image: postgres\n    networks:\n      - sonarnet\n    environment:\n      - POSTGRES_USER=sonar\n      - POSTGRES_PASSWORD=sonar\n    volumes:\n      - postgresql:/var/lib/postgresql\n      - postgresql_data:/var/lib/postgresql/data\n\nnetworks:\n  sonarnet:\n\nvolumes:\n  sonarqube_conf:\n  sonarqube_data:\n  sonarqube_extensions:\n  sonarqube_bundled-plugins:\n  postgresql:\n  postgresql_data:\n```\n\nCopy and paste this into a docker-compose.yml file\n\n```bash\ndocker-compose up\n```\n\nGreat we now have the server running!\n\nNext step is to upload the data. Sonarqube team provides a concept called “scanners” which will help with that.\n\nFor .NET core we can use the dotnet-sonnarscanner which I will show you in this example.\nFor more scanners’ checkout https://docs.sonarqube.org/latest/analysis/overview/\n\nTo install the .NET core scanner we can simply add it though the dotnet CLI.\n\n```bash\ndotnet tool install --global dotnet-sonarscanner\n```\n\nNext step is to use the scanner to upload data to sonarqube server.\n\nTo start, we will tell the scanner to attach itself to the Roslyn compiler.\nWe need to provide /k: “project-key” where project-key is the name of the project on which sonarqube server will place its analysis.\n\n```\ndotnet-sonarscanner begin /k:\"project-key\"\n```\n\nWe trigger the build.\n\n```\ndotnet build\n```\n\nAnd lastly, we turn off the scanner and upload the result. By default, the scanner will look for localhost:9000 which is the same port specified in the docker-compose file.\n\n```\ndotnet-sonarscanner end\n```\n\nThat’s pretty much it. You can now start configuring the rules together with your team. Note though, that you may find already preconfigured templates that can give you a headstart on this. The configuration of rules may be an iterative process so don’t give your self high hopes on the first iteration.\n\nhttps://www.sonarqube.org/\n\nHappy Coding!\n"}]}