<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Swacblooms&#129419;</title>
	<atom:link href="https://swacblooms.com/feed/" rel="self" type="application/rss+xml"/>
	<link>https://swacblooms.com</link>
	<description>Making the Moves</description>
	<lastBuildDate>Mon, 13 Apr 2026 07:15:51 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6.5</generator>

<image>
	<url>https://swacblooms.com/wp-content/uploads/2018/09/cropped-SWACBLOOMS1-32x32.jpg</url>
	<title>Swacblooms&#129419;</title>
	<link>https://swacblooms.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<itunes:explicit>no</itunes:explicit><itunes:subtitle>Making the Moves</itunes:subtitle><item>
		<title>Part 3 — Actions and Auto-Waiting: Putting Locators to Work</title>
		<link>https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=part-3-actions-and-auto-waiting-putting-locators-to-work</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 07:12:56 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1853</guid>

					<description><![CDATA[<p>Introduction In Part 2 you learned how to find elements on the page. In this article, you will put those locators to work by performing actions on them — clicking, typing, hovering, selecting, uploading files, and more. You will also learn one of Playwright&#8217;s most important features:&#160;auto-waiting. Playwright does not blindly fire an action the moment you call it. It waits until the element is ready. This is why Playwright tests are far less flaky than tests written with older tools. Auto-Waiting — Why You Rarely Need&#160;waitFor Before covering individual actions, it is worth understanding what happens under the hood </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/">Part 3 — Actions and Auto-Waiting: Putting Locators to Work</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p>In Part 2 you learned how to find elements on the page. In this article, you will put those locators to work by performing actions on them — clicking, typing, hovering, selecting, uploading files, and more.</p>



<p>You will also learn one of Playwright&#8217;s most important features:&nbsp;<strong>auto-waiting</strong>. Playwright does not blindly fire an action the moment you call it. It waits until the element is ready. This is why Playwright tests are far less flaky than tests written with older tools.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="auto-waiting--why-you-rarely-need-waitfor">Auto-Waiting — Why You Rarely Need&nbsp;<code>waitFor</code></h2>



<p>Before covering individual actions, it is worth understanding what happens under the hood every time you call one.</p>



<p>When you write:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByRole('button', { name: 'Submit' }).click();

</pre></div>


<p>Playwright does not just fire a click immediately. It first checks that the element:</p>



<ul class="wp-block-list">
<li><strong>Exists</strong> in the DOM</li>



<li><strong>Is visible</strong> (not hidden with <code>display: none</code> or <code>visibility: hidden</code>)</li>



<li><strong>Is stable</strong> (not animating or moving)</li>



<li><strong>Is enabled</strong> (not disabled)</li>



<li><strong>Is not obscured</strong> by another element</li>
</ul>



<p>Only when all of those conditions are met does Playwright act. If the element is not ready yet, Playwright keeps retrying until the configured timeout is reached before failing.</p>



<p>This means you rarely need to write:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.waitForSelector('.submit-button'); // rarely needed
await page.waitForTimeout(2000);              // almost never needed

</pre></div>


<p>Just write the action. Playwright handles the waiting.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="clicking">Clicking</h2>



<p><code>click()</code>&nbsp;is the most common action. It simulates a real mouse click — move, press, release.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// single click
await page.getByRole('button', { name: 'Submit' }).click();

// double click
await page.getByRole('button', { name: 'Edit' }).dblclick();

// right click (context menu)
await page.getByRole('listitem', { name: 'File' }).click({ button: 'right' });

// click a specific position within an element
await page.getByRole('canvas').click({ position: { x: 100, y: 200 } });

</pre></div>


<p>If the element is covered by an overlay like a cookie banner, Playwright will throw rather than silently clicking the wrong thing. Fix the root cause — dismiss the overlay first.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="typing-and-filling-inputs">Typing and Filling Inputs</h2>



<p>There are two ways to put text into an input:&nbsp;<code>fill()</code>&nbsp;and&nbsp;<code>pressSequentially()</code>.</p>



<h3 class="wp-block-heading" id="fill--use-this-first"><code>fill()</code>&nbsp;— Use This First</h3>



<p><code>fill()</code>&nbsp;clears the current value and sets the new one in a single operation. It is fast and reliable.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Password').fill('secret123');

</pre></div>


<h3 class="wp-block-heading" id="presssequentially--when-you-need-key-by-key-input"><code>pressSequentially()</code>&nbsp;— When You Need Key-by-Key Input</h3>



<p>Some inputs respond to individual keystrokes — autocomplete dropdowns, for example.&nbsp;<code>pressSequentially()</code>&nbsp;types character by character, triggering keyboard events as it goes.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel('Search').pressSequentially('play', { delay: 50 });

</pre></div>


<p>The&nbsp;<code>delay</code>&nbsp;option adds a pause between keystrokes in milliseconds. Without it the typing is instantaneous, which some inputs do not handle correctly.</p>



<h3 class="wp-block-heading" id="clear--emptying-a-field"><code>clear()</code>&nbsp;— Emptying a Field</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel('Username').clear();

</pre></div>


<h3 class="wp-block-heading" id="press--single-key-presses"><code>press()</code>&nbsp;— Single Key Presses</h3>



<p>Use&nbsp;<code>press()</code>&nbsp;when you need to hit a specific key — Enter, Tab, Escape, or an arrow key.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByLabel('Search').press('Enter');
await page.getByRole('textbox').press('Tab');
await page.getByRole('dialog').press('Escape');

</pre></div>


<p>Key names follow the&nbsp;<a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">KeyboardEvent.key</a>&nbsp;standard. Common ones:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Key</th><th>String to use</th></tr></thead><tbody><tr><td>Enter</td><td><code>'Enter'</code></td></tr><tr><td>Tab</td><td><code>'Tab'</code></td></tr><tr><td>Escape</td><td><code>'Escape'</code></td></tr><tr><td>Arrow keys</td><td><code>'ArrowUp'</code>&nbsp;/&nbsp;<code>'ArrowDown'</code>&nbsp;/&nbsp;<code>'ArrowLeft'</code>&nbsp;/&nbsp;<code>'ArrowRight'</code></td></tr><tr><td>Backspace</td><td><code>'Backspace'</code></td></tr><tr><td>Modifier + key</td><td><code>'Control+a'</code>,&nbsp;<code>'Meta+c'</code></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="checkboxes-and-radio-buttons">Checkboxes and Radio Buttons</h2>



<p>Use&nbsp;<code>check()</code>&nbsp;and&nbsp;<code>uncheck()</code>&nbsp;rather than&nbsp;<code>click()</code>&nbsp;for checkboxes and radio buttons. They are more explicit and Playwright verifies the resulting state.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// check a checkbox
await page.getByLabel('Remember me').check();

// uncheck it
await page.getByLabel('Remember me').uncheck();

// select a radio button
await page.getByLabel('Credit card').check();

</pre></div>


<p>To verify the current state:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await expect(page.getByLabel('Remember me')).toBeChecked();
await expect(page.getByLabel('Remember me')).not.toBeChecked();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="dropdowns--selectoption">Dropdowns —&nbsp;<code>selectOption()</code></h2>



<p>For a native&nbsp;<code>&lt;select&gt;</code>&nbsp;element, use&nbsp;<code>selectOption()</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// select by visible text
await page.getByLabel('Country').selectOption('United Kingdom');

// select by value attribute
await page.getByLabel('Country').selectOption({ value: 'uk' });

// select by index (zero-based)
await page.getByLabel('Country').selectOption({ index: 2 });

// select multiple options (if the select allows it)
await page.getByLabel('Interests').selectOption(&#91;'Music', 'Sport']);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="hovering">Hovering</h2>



<p><code>hover()</code>&nbsp;moves the mouse over an element without clicking. Use it to trigger tooltips, reveal dropdown menus, or test hover states.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole('button', { name: 'More options' }).hover();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="focus-and-blur">Focus and Blur</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// move keyboard focus to an element
await page.getByLabel('Email').focus();

// remove focus
await page.getByLabel('Email').blur();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="file-uploads">File Uploads</h2>



<p><code>setInputFiles()</code>&nbsp;sets files on an&nbsp;<code>&lt;input type="file"&gt;</code>&nbsp;element.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// upload a single file
await page.getByLabel('Upload document').setInputFiles('path/to/file.pdf');

// upload multiple files
await page.getByLabel('Upload photos').setInputFiles(&#91;
  'photo1.jpg',
  'photo2.jpg',
]);

// clear a previously selected file
await page.getByLabel('Upload document').setInputFiles(&#91;]);

</pre></div>


<p>For upload dialogs triggered by a button rather than a direct file input:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
const &#91;fileChooser] = await Promise.all(&#91;
  page.waitForEvent('filechooser'),
  page.getByRole('button', { name: 'Upload' }).click(),
]);
await fileChooser.setFiles('path/to/file.pdf');
</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="drag-and-drop">Drag and Drop</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole('listitem', { name: 'Task A' }).dragTo(
  page.getByRole('region', { name: 'Done' })
);

</pre></div>


<p>For fine-grained control:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
await page.getByRole('slider').dragTo(
  page.getByRole('slider'),
  { targetPosition: { x: 200, y: 0 } }
);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="scrolling">Scrolling</h2>



<p>Playwright scrolls automatically when it needs to bring an element into view before acting. For explicit scrolling:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// scroll an element into view
await page.getByRole('heading', { name: 'Pricing' }).scrollIntoViewIfNeeded();

// scroll the page by a pixel amount
await page.mouse.wheel(0, 500);

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p>In Part 3, you learned how to interact with the page in a reliable way.</p>



<ul class="wp-block-list">
<li>Playwright waits for elements to be ready before it performs an action.</li>



<li><code>fill()</code> is the default choice for text inputs, while <code>pressSequentially()</code> is useful when key-by-key typing is required.</li>



<li><code>check()</code> and <code>uncheck()</code> are the right tools for checkboxes and radio buttons.</li>



<li><code>selectOption()</code> is used for native <code>&lt;select></code> dropdowns.</li>



<li><code>press()</code> lets you send single keys and key combinations such as <code>Control+a</code>.</li>



<li><code>setInputFiles()</code> works for file inputs, and the <code>filechooser</code> event pattern works for upload buttons.</li>



<li>In most tests, you can skip manual waits like <code>waitForTimeout</code>.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p>In&nbsp;<strong>Part 4</strong>&nbsp;you will learn assertions — how to verify that the right things appear on the page after your actions. Playwright&#8217;s web-first assertions retry automatically, and you will learn why that matters and how to use them correctly.</p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/part-3-actions-and-auto-waiting-putting-locators-to-work/">Part 3 — Actions and Auto-Waiting: Putting Locators to Work</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Part 2 — Understanding Locators: Finding Things on the Page</title>
		<link>https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=part-2-understanding-locators-finding-things-on-the-page</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 06:21:56 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1849</guid>

					<description><![CDATA[<p>Introduction Before you can click a button, fill a form, or assert that something is visible, you need to tell Playwright&#160;which&#160;element you are talking about. That is what locators are for. A locator is not just a selector — it is a smart reference to an element that re-queries the DOM every time you use it. This means if the page re-renders between steps, Playwright finds the fresh element automatically rather than holding a stale reference that no longer exists. In this article, you will learn some of the most common locator methods Playwright provides, when to use each one, </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/">Part 2 — Understanding Locators: Finding Things on the Page</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p>Before you can click a button, fill a form, or assert that something is visible, you need to tell Playwright&nbsp;<em>which</em>&nbsp;element you are talking about. That is what locators are for.</p>



<p>A locator is not just a selector — it is a smart reference to an element that re-queries the DOM every time you use it. This means if the page re-renders between steps, Playwright finds the fresh element automatically rather than holding a stale reference that no longer exists.</p>



<p>In this article, you will learn some of the most common locator methods Playwright provides, when to use each one, and the priority order the Playwright team recommends.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="recommended-built-in-locators">Recommended Built-in Locators</h2>



<p>Playwright&#8217;s docs recommend these built-in locators over CSS and XPath because they are tied to what users actually see and interact with:</p>



<ol class="wp-block-list">
<li><code>getByRole</code></li>



<li><code>getByText</code></li>



<li><code>getByLabel</code></li>



<li><code>getByPlaceholder</code></li>



<li><code>getByAltText</code></li>



<li><code>getByTitle</code></li>



<li><code>getByTestId</code></li>
</ol>



<p>The guiding principle:&nbsp;<strong>the closer a locator is to how a real user perceives the page, the better.</strong>&nbsp;Users read text and interact with buttons and links — they do not think in CSS class names.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyrole--start-here"><code>getByRole</code>&nbsp;— Start Here</h2>



<p><code>getByRole</code>&nbsp;finds elements by their ARIA role and accessible name. This is the recommended first choice for almost every interactive element on the page.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds &lt;a href=&quot;/docs/intro&quot;&gt;Get started&lt;/a&gt;
page.getByRole('link', { name: 'Get started' })

// finds &lt;button&gt;Submit&lt;/button&gt;
page.getByRole('button', { name: 'Submit' })

// finds &lt;h1&gt;Installation&lt;/h1&gt;
page.getByRole('heading', { name: 'Installation' })

// finds &lt;input type=&quot;checkbox&quot;&gt; with label &quot;Remember me&quot;
page.getByRole('checkbox', { name: 'Remember me' })

</pre></div>


<p>The role comes from the HTML element itself — no&nbsp;<code>aria-*</code>&nbsp;attributes needed. The browser assigns roles automatically:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>HTML element</th><th>Implicit ARIA role</th></tr></thead><tbody><tr><td><code>&lt;a href="..."&gt;</code></td><td><code>link</code></td></tr><tr><td><code>&lt;button&gt;</code></td><td><code>button</code></td></tr><tr><td><code>&lt;h1&gt;</code>&nbsp;–&nbsp;<code>&lt;h6&gt;</code></td><td><code>heading</code></td></tr><tr><td><code>&lt;input type="checkbox"&gt;</code></td><td><code>checkbox</code></td></tr><tr><td><code>&lt;input type="text"&gt;</code></td><td><code>textbox</code></td></tr><tr><td><code>&lt;img&gt;</code></td><td><code>img</code></td></tr><tr><td><code>&lt;ul&gt;</code>&nbsp;/&nbsp;<code>&lt;ol&gt;</code></td><td><code>list</code></td></tr></tbody></table></figure>



<p>The accessible name comes from the element&#8217;s visible text content (as you saw in Part 1 with the&nbsp;<code>Get started</code>&nbsp;link). If the element has an&nbsp;<code>aria-label</code>&nbsp;or&nbsp;<code>aria-labelledby</code>, that takes priority over text content.</p>



<p>The&nbsp;<code>name</code>&nbsp;option accepts a string or a regular expression:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// exact string match
page.getByRole('button', { name: 'Sign in' })

// regex — useful when the text is dynamic
page.getByRole('button', { name: /sign in/i })

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytext--for-non-interactive-elements"><code>getByText</code>&nbsp;— For Non-Interactive Elements</h2>



<p>Use&nbsp;<code>getByText</code>&nbsp;when you want to find an element by its visible text content and there is no meaningful role to target — typically a&nbsp;<code>&lt;div&gt;</code>,&nbsp;<code>&lt;span&gt;</code>, or&nbsp;<code>&lt;p&gt;</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds any element containing the text &quot;Welcome back&quot;
page.getByText('Welcome back')

// exact match only
page.getByText('Welcome back', { exact: true })

// regex
page.getByText(/welcome back/i)

</pre></div>


<p><strong>Important:</strong>&nbsp;Playwright normalises whitespace automatically, even with&nbsp;<code>{ exact: true }</code>. Multiple spaces become one, line breaks become spaces, and leading/trailing whitespace is ignored.</p>



<p>Avoid using&nbsp;<code>getByText</code>&nbsp;on buttons and links — use&nbsp;<code>getByRole</code>&nbsp;for those.&nbsp;<code>getByText</code>&nbsp;is for reading content, not for triggering actions.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbylabel--for-form-fields"><code>getByLabel</code>&nbsp;— For Form Fields</h2>



<p><code>getByLabel</code>&nbsp;finds an input by the text of its associated&nbsp;<code>&lt;label&gt;</code>. This is the right tool any time you are filling in a form.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// finds the input associated with &lt;label&gt;Email address&lt;/label&gt;
page.getByLabel('Email address')

// usage in a test
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Password').fill('secret');

</pre></div>


<p>The label association can be done two ways in HTML, and&nbsp;<code>getByLabel</code>&nbsp;handles both:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&lt;!-- wrapping label --&gt;
&lt;label&gt;
  Email address
  &lt;input type=&quot;email&quot; /&gt;
&lt;/label&gt;

&lt;!-- linked by for/id --&gt;
&lt;label for=&quot;email&quot;&gt;Email address&lt;/label&gt;
&lt;input id=&quot;email&quot; type=&quot;email&quot; /&gt;

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyplaceholder--when-there-is-no-label"><code>getByPlaceholder</code>&nbsp;— When There Is No Label</h2>



<p>Some inputs skip a visible label and rely on placeholder text instead. Use&nbsp;<code>getByPlaceholder</code>&nbsp;for those.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&lt;input type=&quot;email&quot; placeholder=&quot;Enter your email&quot; /&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByPlaceholder('Enter your email').fill('user@example.com');

</pre></div>


<p>Prefer&nbsp;<code>getByLabel</code>&nbsp;over&nbsp;<code>getByPlaceholder</code>&nbsp;when both are available — a visible label is better for accessibility and more stable as a test target.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbyalttext--for-images"><code>getByAltText</code>&nbsp;— For Images</h2>



<p><code>getByAltText</code>&nbsp;finds elements by their&nbsp;<code>alt</code>&nbsp;attribute. In practice this means images.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&lt;img src=&quot;logo.png&quot; alt=&quot;Company logo&quot; /&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await expect(page.getByAltText('Company logo')).toBeVisible();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytitle--for-title-attributes"><code>getByTitle</code>&nbsp;— For Title Attributes</h2>



<p><code>getByTitle</code>&nbsp;matches elements with a&nbsp;<code>title</code>&nbsp;attribute. Less common, but useful for icon buttons and tooltips that carry no visible text.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&lt;button title=&quot;Close dialog&quot;&gt;✕&lt;/button&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByTitle('Close dialog').click();

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="getbytestid--the-explicit-contract"><code>getByTestId</code>&nbsp;— The Explicit Contract</h2>



<p><code>getByTestId</code>&nbsp;finds elements by a&nbsp;<code>data-testid</code>&nbsp;attribute (or a custom attribute you configure).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
&lt;button data-testid=&quot;submit-order&quot;&gt;Place order&lt;/button&gt;

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await page.getByTestId('submit-order').click();

</pre></div>


<p>This locator is the most resilient to UI changes — the visible text and styling can change entirely without breaking your test. The trade-off: it requires you to add&nbsp;<code>data-testid</code>&nbsp;attributes to your HTML, and it is not based on anything a real user perceives.</p>



<p>Use it when:</p>



<ul class="wp-block-list">
<li>A role or text locator is not specific enough</li>



<li>You want an explicit, stable contract between your tests and a specific element</li>



<li>The element has no accessible role or visible text</li>
</ul>



<p>You can configure a custom attribute name in&nbsp;<code>playwright.config.js</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
use: {
  testIdAttribute: 'data-cy', // use data-cy instead of data-testid
}

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="css-and-xpath--last-resort">CSS and XPath — Last Resort</h2>



<p>When none of the above fit, you can fall back to CSS selectors or XPath via&nbsp;<code>locator()</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// ID selector
page.locator('#submit-button')

// CSS selector
page.locator('.nav-menu &gt; li:first-child')

// XPath
page.locator('//button&#91;@type=&quot;submit&quot;]')

</pre></div>


<p>Playwright auto-detects whether you passed CSS or XPath — no prefix needed.</p>



<p><strong>Why avoid these?</strong>&nbsp;The DOM changes. Class names get renamed, elements get restructured, and your tests break for reasons unrelated to the actual behaviour of the app. The role and text-based locators are far more stable because they are tied to what users see, not to implementation details.</p>



<p>One additional limitation:&nbsp;<strong>XPath does not pierce Shadow DOM.</strong>&nbsp;All other Playwright locators work inside shadow roots by default.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="filtering-locators">Filtering Locators</h2>



<p>When a locator matches more than one element, use&nbsp;<code>filter()</code>&nbsp;to narrow it down.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// all list items that contain the text &quot;In stock&quot;
page.getByRole('listitem').filter({ hasText: 'In stock' })

// list items that do NOT contain &quot;Sold out&quot;
page.getByRole('listitem').filter({ hasNotText: 'Sold out' })

// list items that contain a child button labelled &quot;Add to cart&quot;
page.getByRole('listitem').filter({
  has: page.getByRole('button', { name: 'Add to cart' })
})

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="chaining-locators">Chaining Locators</h2>



<p>All locator methods are also available on a locator itself, so you can scope a search inside a specific part of the page.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// find the navigation section first, then look for a link inside it
page.getByRole('navigation').getByRole('link', { name: 'Pricing' })

// find a form, then find the submit button inside it
page.getByRole('form', { name: 'Checkout' }).getByRole('button', { name: 'Place order' })

</pre></div>


<p>This is cleaner and more precise than a deeply nested CSS selector, and it reads like plain English.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="strictness--one-element-at-a-time">Strictness — One Element at a Time</h2>



<p>Playwright locators are strict by default. If a locator matches more than one element and you call an action on it (like&nbsp;<code>.click()</code>), Playwright throws an error rather than silently picking the first match.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error: locator.click: Error: strict mode violation:
getByRole('button', { name: 'Submit' }) resolved to 2 elements

</pre></div>


<p>This is intentional — it forces you to write specific locators. The fix is to make the locator more precise rather than reaching for&nbsp;<code>.first()</code>&nbsp;or&nbsp;<code>.nth(0)</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// vague — which Submit button?
page.getByRole('button', { name: 'Submit' })

// specific — scoped to the checkout form
page.getByRole('form', { name: 'Checkout' }).getByRole('button', { name: 'Submit' })

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p>Here is what you learned in Part 2:</p>



<ul class="wp-block-list">
<li>A locator re-queries the DOM every time it is used — no stale element references</li>



<li>Use <code>getByRole</code> first: it targets elements the same way users and screen readers do</li>



<li>Use <code>getByLabel</code> for form inputs, <code>getByText</code> for static content, <code>getByAltText</code> for images</li>



<li>Use <code>getByTestId</code> when you need an explicit, stable testing contract</li>



<li>Avoid CSS and XPath unless nothing else fits — they are tied to implementation details</li>



<li><code>filter()</code> and chaining let you narrow down locators precisely</li>



<li>Locators are strict: if more than one element matches, Playwright errors rather than guessing</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p>In <strong>Part 3</strong> we put locators to work. You will learn some of the actions Playwright can perform — clicking, typing, hovering, selecting, uploading files — and how Playwright&#8217;s auto-waiting means you almost never need to add a manual wait.</p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/part-2-understanding-locators-finding-things-on-the-page/">Part 2 — Understanding Locators: Finding Things on the Page</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>What Is Playwright and Why Should You Care?</title>
		<link>https://swacblooms.com/what-is-playwright-and-why-should-you-care/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=what-is-playwright-and-why-should-you-care</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 23 Mar 2026 06:31:03 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1845</guid>

					<description><![CDATA[<p>Introduction If you have ever shipped a feature that looked perfect in your local browser but broke immediately in production, you already understand why automated browser testing exists. Playwright is the answer to the question: &#8220;How do we make that kind of testing fast, reliable, and not painful to write?&#8221; This first article walks you through what Playwright is, why it stands out from its competitors, how to install it, and how to read your very first test. By the end, you will have a working setup and a solid mental model of what each piece does. What Is Playwright? Playwright </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/what-is-playwright-and-why-should-you-care/">What Is Playwright and Why Should You Care?</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="introduction">Introduction</h2>



<p>If you have ever shipped a feature that looked perfect in your local browser but broke immediately in production, you already understand why automated browser testing exists. Playwright is the answer to the question: <em>&#8220;How do we make that kind of testing fast, reliable, and not painful to write?&#8221;</em></p>



<p>This first article walks you through what Playwright is, why it stands out from its competitors, how to install it, and how to read your very first test. By the end, you will have a working setup and a solid mental model of what each piece does.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="what-is-playwright">What Is Playwright?</h2>



<p>Playwright is an open-source end-to-end (E2E) testing framework built by Microsoft. &#8220;End-to-end&#8221; means it controls a real browser — Chrome, Firefox, or Safari — the same way a real user would: clicking links, filling forms, navigating pages, and checking that the right content appears.</p>



<p>Unlike unit tests (which test isolated functions) or integration tests (which test modules together), E2E tests validate the entire stack from the UI down to the database and back. They are the closest thing to&nbsp;<em>&#8220;a human actually using your app.&#8221;</em></p>



<p>Playwright supports:</p>



<ul class="wp-block-list">
<li>Chromium (Chrome / Edge)</li>



<li>Firefox</li>



<li>WebKit (Safari)</li>



<li>Mobile viewports</li>



<li>Multiple languages: JavaScript, TypeScript, Python, Java, .NET</li>
</ul>



<p>This series uses JavaScript/TypeScript.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="why-playwright">Why Playwright?</h2>



<p>There are other browser automation tools out there — Selenium and Cypress being the most well-known. This series focuses on Playwright, and here is what makes it worth your time:</p>



<ul class="wp-block-list">
<li>Supports all three major browser engines — Chromium, Firefox, and WebKit — out of the box</li>



<li>Auto-waits for elements to be ready before acting, so no manual sleeps or brittle waits</li>



<li>Runs tests in parallel across browsers simultaneously</li>



<li>First-class TypeScript support with no extra configuration</li>



<li>Built-in network interception, trace recording, and a visual debugger</li>



<li>Actively maintained by Microsoft with frequent releases</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="installing-playwright">Installing Playwright</h2>



<p>Playwright requires Node.js 20.x, 22.x, or 24.x. Check your version:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
node --version

</pre></div>


<p>To scaffold a new Playwright project, run:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
npm init playwright@latest

</pre></div>


<p>The CLI will ask a few questions:</p>



<ul class="wp-block-list">
<li>TypeScript or JavaScript? <em>(choose either — this series uses both)</em></li>



<li>Where to put your tests? <em>(default: <code>tests/</code>)</em></li>



<li>Add a GitHub Actions workflow? <em>(yes — we cover this in Part 11)</em></li>



<li>Install Playwright browsers? <em>(yes)</em></li>
</ul>



<p>That last step downloads Chromium, Firefox, and WebKit — about 300 MB total. Playwright ships its own browser binaries, so your system browsers never interfere.</p>



<p>After installation, your project looks like this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
your-project/
├── node_modules/
├── tests/
│   └── example.spec.js       ← your starter test
├── playwright.config.js      ← all configuration lives here
├── package.json
└── package-lock.json

</pre></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="your-first-test--line-by-line">Your First Test — Line by Line</h2>



<p>Open&nbsp;<code>tests/example.spec.js</code>. Here is what it contains:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// @ts-check
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) =&gt; {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) =&gt; {
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

</pre></div>


<p>Let&#8217;s break each part down.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="-ts-check"><code>// @ts-check</code></h3>



<p>A JSDoc comment that tells VS Code to type-check this plain JavaScript file using TypeScript&#8217;s engine. You get autocomplete and inline errors without needing a&nbsp;<code>tsconfig</code>. Optional, but useful.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="import--test-expect--from-playwrighttest"><code>import { test, expect } from '@playwright/test'</code></h3>



<p>Two things are imported:</p>



<ul class="wp-block-list">
<li><code>test</code> — the function you use to define a test case (like <code>it()</code> in Jest)</li>



<li><code>expect</code> — the assertion library (like Jest&#8217;s <code>expect</code>, extended with browser-aware matchers)</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="testhas-title-async--page-----"><code>test('has title', async ({ page }) =&gt; { ... })</code></h3>



<ul class="wp-block-list">
<li>The first argument is the <strong>test name</strong> — what shows up in the report.</li>



<li>The second argument is an async function that receives <strong>fixtures</strong>. The <code>page</code> fixture is Playwright&#8217;s central object: it represents a single browser tab.</li>



<li>Every browser interaction is async, so you use <code>await</code> throughout.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-pagegotohttpsplaywrightdev"><code>await page.goto('https://playwright.dev/')</code></h3>



<p>Navigates the browser to a URL. Playwright waits for the page to reach a &#8220;load&#8221; state before moving on.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-expectpagetohavetitleplaywright"><code>await expect(page).toHaveTitle(/Playwright/)</code></h3>



<p>Asserts that the page&#8217;s&nbsp;<code>&lt;title&gt;</code>&nbsp;tag matches the regular expression&nbsp;<code>/Playwright/</code>. This is a&nbsp;<strong>web-first assertion</strong>&nbsp;— if the title is not there yet, Playwright retries the check for up to 5 seconds before failing. No manual waits needed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-pagegetbyrolelink--name-get-started-click"><code>await page.getByRole('link', { name: 'Get started' }).click()</code></h3>



<p>Finds an&nbsp;<code>&lt;a&gt;</code>&nbsp;element whose accessible name is &#8220;Get started&#8221; and clicks it.&nbsp;<code>getByRole</code>&nbsp;is the preferred way to select elements — it mirrors how assistive technologies see your page and is far more resilient than CSS selectors.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="await-expectpagegetbyroleheading--name-installation-tobevisible"><code>await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible()</code></h3>



<p>After clicking, the test navigates to the installation page. This assertion checks that a heading with the text &#8220;Installation&#8221; becomes visible. Again, Playwright retries this automatically.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="understanding-the-playwrightconfigjs-file">Understanding the&nbsp;<code>playwright.config.js</code>&nbsp;File</h2>



<p>The&nbsp;<code>playwright.config.js</code>&nbsp;file controls how your entire test suite runs. The default configuration sets up three test projects — one for each browser engine.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: &#91;
    { name: 'chromium', use: { ...devices&#91;'Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices&#91;'Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices&#91;'Desktop Safari'] } },
  ],
});

</pre></div>


<p>Key settings to know right now:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>What it does</th></tr></thead><tbody><tr><td><code>testDir</code></td><td>Where Playwright looks for test files (<code>*.spec.js</code>&nbsp;/&nbsp;<code>*.test.js</code>)</td></tr><tr><td><code>fullyParallel</code></td><td>Run all tests in all files at the same time — faster, but tests must not share state</td></tr><tr><td><code>retries</code></td><td>How many times to retry a failing test. Set to&nbsp;<code>2</code>&nbsp;on CI to reduce flakiness noise</td></tr><tr><td><code>reporter</code></td><td><code>"html"</code>&nbsp;generates a rich clickable report in&nbsp;<code>playwright-report/</code></td></tr><tr><td><code>trace</code></td><td>Records screenshots, network, and console logs on the first retry of a failure</td></tr><tr><td><code>projects</code></td><td>Each project runs the full suite in one browser — your tests automatically run 3×</td></tr></tbody></table></figure>



<p>We will explore every option in detail in Part 5.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="running-your-tests">Running Your Tests</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# Run the full suite across all browsers
npx playwright test

# Run only in one browser
npx playwright test --project=chromium

# Run a specific file
npx playwright test tests/example.spec.js

# Watch the browser open (headed mode)
npx playwright test --headed

# Open the interactive HTML report after a run
npx playwright show-report

# Open UI Mode — live debugging with a built-in browser and test explorer
npx playwright test --ui

</pre></div>


<p>When tests pass, you will see:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Running 6 tests using 3 workers
  6 passed (8.3s)

</pre></div>


<p>That &#8220;6 tests&#8221; comes from&nbsp;<strong>2 test cases × 3 browser projects</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="reading-a-failure">Reading a Failure</h2>



<p>Break one of the assertions intentionally:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
await expect(page).toHaveTitle(/NotARealTitle/);

</pre></div>


<p>Run again. Playwright will print:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Error: expect(page).toHaveTitle(expected)

Expected pattern: /NotARealTitle/
Received string:  &quot;Fast and reliable end-to-end testing for modern web apps | Playwright&quot;

</pre></div>


<p>The diff is clear and actionable. The HTML report adds a screenshot of the page at the moment of failure, and if tracing is enabled, a full step-by-step replay.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="summary">Summary</h2>



<p>Here is what you learned in Part 1:</p>



<ul class="wp-block-list">
<li>Playwright is an E2E testing framework that drives real browsers</li>



<li>It supports Chromium, Firefox, and WebKit with auto-waiting built in</li>



<li><code>npm init playwright@latest</code> scaffolds a working project in seconds</li>



<li>A test is an async function that receives a <code>page</code> fixture</li>



<li><code>expect()</code> assertions are web-first: they retry until they pass or time out</li>



<li><code>playwright.config.js</code> controls browsers, parallelism, retries, and more</li>



<li><code>npx playwright test</code> runs everything; <code>--project</code> and <code>--headed</code> are useful flags</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="whats-next">What&#8217;s Next</h2>



<p>In <strong>Part 2,</strong> we dive deep into locators — the API you will use in every single test to find elements on the page. You will learn why <code>getByRole</code> is almost always the right choice, and when to reach for other strategies.</p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/what-is-playwright-and-why-should-you-care/">What Is Playwright and Why Should You Care?</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>WebRTC Renegotiation: When You Need It and When You Don’t</title>
		<link>https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=webrtc-renegotiation-when-you-need-it-and-when-you-dont</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 08:42:50 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1831</guid>

					<description><![CDATA[<p>The Question After implementing track toggling with&#160;replaceTrack(), a crucial question emerges:&#160;When do I need to renegotiate (create a new offer/answer) vs when can I just swap tracks? Understanding this is critical for building efficient WebRTC applications. Unnecessary renegotiation adds latency and complexity, while missing required renegotiation breaks your connection. Quick Answer Use&#160;replaceTrack()&#160;(no renegotiation)&#160;when: Renegotiate (new offer/answer)&#160;when: What is a Transceiver? A transceiver is a&#160;bidirectional media slot&#160;in your peer connection: Key insight: Once a transceiver exists, you can swap tracks in/out without renegotiation. The slot is reserved for the connection lifetime. The SDP Connection Renegotiation is required when the&#160;SDP (Session Description </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/">WebRTC Renegotiation: When You Need It and When You Don&#8217;t</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><strong>The Question</strong></p>



<p>After implementing track toggling with&nbsp;<code>replaceTrack()</code>, a crucial question emerges:&nbsp;<strong>When do I need to renegotiate (create a new offer/answer) vs when can I just swap tracks?</strong></p>



<p>Understanding this is critical for building efficient WebRTC applications. Unnecessary renegotiation adds latency and complexity, while missing required renegotiation breaks your connection.</p>



<p><strong>Quick Answer</strong></p>



<p><strong>Use&nbsp;<code>replaceTrack()</code>&nbsp;(no renegotiation)</strong>&nbsp;when:</p>



<ul class="wp-block-list">
<li>Swapping tracks of the same type (camera 1 → camera 2)</li>



<li>Toggling tracks on/off (video → null → video)</li>



<li>The transceiver slot already exists</li>
</ul>



<p><strong>Renegotiate (new offer/answer)</strong>&nbsp;when:</p>



<ul class="wp-block-list">
<li>Adding new transceivers (new media types)</li>



<li>Changing transceiver direction</li>



<li>Stopping transceivers permanently</li>



<li>Any structural SDP changes</li>
</ul>



<h3 class="wp-block-heading" id="what-is-a-transceiver">What is a Transceiver?</h3>



<p>A transceiver is a&nbsp;<strong>bidirectional media slot</strong>&nbsp;in your peer connection:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Creates a slot for video communication
const transceiver = peerConnection.addTransceiver('video', {
  direction: 'sendrecv'  // Can send AND receive
})

// The transceiver has:
// - sender: sends your video to the peer
// - receiver: receives peer's video
// - direction: sendrecv, sendonly, recvonly, inactive
</pre></div>


<p><strong>Key insight</strong>: Once a transceiver exists, you can swap tracks in/out without renegotiation. The slot is reserved for the connection lifetime.</p>



<h3 class="wp-block-heading" id="the-sdp-connection">The SDP Connection</h3>



<p>Renegotiation is required when the&nbsp;<strong>SDP (Session Description Protocol) structure changes</strong>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97    ← Video m-line (transceiver slot)
m=audio 9 UDP/TLS/RTP/SAVPF 111      ← Audio m-line (transceiver slot)
</pre></div>


<ul class="wp-block-list">
<li>Each&nbsp;<code>m=</code>&nbsp;line represents a transceiver</li>



<li>Adding/removing m-lines = renegotiation required</li>



<li>Changing direction in m-line = renegotiation required</li>



<li>Swapping tracks within existing m-line =&nbsp;<strong>NO</strong>&nbsp;renegotiation needed</li>
</ul>



<h2 class="wp-block-heading" id="scenarios-no-renegotiation-needed-">Scenarios: No Renegotiation Needed <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></h2>



<h3 class="wp-block-heading" id="1-camera-toggling-on--off--on">1. Camera Toggling (On → Off → On)</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
let videoSender: RTCRtpSender | null = null

// Initial setup: create transceiver
const videoTransceiver = peerA.addTransceiver(videoTrack, {
  direction: 'sendrecv',
  streams: &#91;localStream]
})
videoSender = videoTransceiver.sender

// Later: Turn camera OFF
await videoSender.replaceTrack(null)  //  NO renegotiation

// Later: Turn camera ON
const newStream = await getUserMedia({ video: true })
const newTrack = newStream.getVideoTracks()&#91;0]
await videoSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p><strong>Why it works</strong>: The video transceiver slot exists. We&#8217;re just changing what goes into it (track → null → track).</p>



<h3 class="wp-block-heading" id="2-switching-cameras">2. Switching Cameras</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
// Front camera → Back camera
const constraints = { video: { facingMode: 'environment' } }
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getVideoTracks()&#91;0]

await videoSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p><strong>Why it works</strong>: Same transceiver, different video source.</p>



<h3 class="wp-block-heading" id="3-switching-microphones">3. Switching Microphones</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Default mic → Headset mic
const constraints = { audio: { deviceId: headsetMicId } }
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getAudioTracks()&#91;0]

await audioSender.replaceTrack(newTrack)  //  NO renegotiation
</pre></div>


<p><strong>Why it works</strong>: Same transceiver, different audio source.</p>



<h3 class="wp-block-heading" id="4-changing-video-resolutionframe-rate">4. Changing Video Resolution/Frame Rate</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// 720p → 1080p
const constraints = {
  video: { width: 1920, height: 1080, frameRate: 30 }
}
const newStream = await getUserMedia(constraints)
const newTrack = newStream.getVideoTracks()&#91;0]

await videoSender.replaceTrack(newTrack)  //  NO renegotiation

</pre></div>


<p><strong>Why it works</strong>: Still using the same video transceiver.</p>



<h2 class="wp-block-heading" id="scenarios-renegotiation-required-">Scenarios: Renegotiation Required <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></h2>



<h3 class="wp-block-heading" id="1-adding-screen-sharing">1. Adding Screen Sharing</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial: video + audio
const videoTransceiver = peerA.addTransceiver(videoTrack, {
  direction: 'sendrecv',
  streams: &#91;localStream]
})
const audioTransceiver = peerA.addTransceiver(audioTrack, {
  direction: 'sendrecv',
  streams: &#91;localStream]
})

// Create offer/answer...

// Later: Add screen sharing (NEW transceiver)
const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true
})
const screenTrack = screenStream.getVideoTracks()&#91;0]

// Adding a new transceiver
const screenTransceiver = peerA.addTransceiver(screenTrack, {
  direction: 'sendrecv',
  streams: &#91;screenStream]
})

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)

// Send offer to peer B
// Wait for answer from peer B
const answer = await receiveAnswerFromPeerB()
await peerA.setRemoteDescription(answer)

</pre></div>


<p><strong>Why renegotiation is needed</strong>: We added a&nbsp;<strong>new m-line</strong>&nbsp;to the SDP. Peer B needs to know about this new media stream.</p>



<h3 class="wp-block-heading" id="2-changing-transceiver-direction">2. Changing Transceiver Direction</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial: bidirectional
const transceiver = peerA.addTransceiver('video', {
  direction: 'sendrecv'  // Can send AND receive
})

// Later: Make it send-only
transceiver.direction = 'sendonly'

// MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p><strong>Why renegotiation is needed</strong>: The SDP m-line direction attribute changed. Peer B needs to update its expectations.</p>



<h3 class="wp-block-heading" id="3-stopping-a-transceiver">3. Stopping a Transceiver</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
const transceiver = peerA.getTransceivers()&#91;0]

transceiver.stop()  // Permanently stops the transceiver

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p><strong>Why renegotiation is needed</strong>: The m-line is marked as inactive in SDP. This is a structural change.</p>



<p><strong>Note</strong>: You rarely need to stop transceivers. Usually&nbsp;<code>replaceTrack(null)</code>&nbsp;is sufficient.</p>



<h3 class="wp-block-heading" id="4-adding-data-channels-after-connection">4. Adding Data Channels After Connection</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Initial connection with video/audio

// Later: Add data channel
const dataChannel = peerA.createDataChannel('fileTransfer')

//  MUST renegotiate!
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
// ... complete negotiation

</pre></div>


<p><strong>Why renegotiation is needed</strong>: Data channels add application m-lines to SDP.</p>



<h2 class="wp-block-heading" id="performance-implications">Performance Implications</h2>



<h3 class="wp-block-heading" id="replacetrack-no-renegotiation"><code>replaceTrack()</code>&nbsp;(No Renegotiation)</h3>



<p><strong>Performance</strong>:</p>



<ul class="wp-block-list">
<li>Near-instant (typically &lt; 50ms)</li>



<li>No network round-trip</li>



<li>No SDP parsing</li>



<li>Seamless for users</li>
</ul>



<p><strong>Use for</strong>:</p>



<ul class="wp-block-list">
<li>Real-time track toggling</li>



<li>Device switching</li>



<li>Quality changes</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Fast and seamless
console.time('replaceTrack')
await videoSender.replaceTrack(newTrack)
console.timeEnd('replaceTrack')  // ~10-30ms

</pre></div>


<h3 class="wp-block-heading" id="renegotiation-offeranswer">Renegotiation (Offer/Answer)</h3>



<p><strong>Performance</strong>:</p>



<ul class="wp-block-list">
<li>Slow (typically 500ms &#8211; 2000ms)</li>



<li>Multiple network round-trip</li>



<li>SDP parsing on both sides</li>



<li>Potential for brief disruption</li>
</ul>



<p><strong>Steps involved</strong>:</p>



<ol class="wp-block-list">
<li>Create offer (100-200ms)</li>



<li>Set local description (50-100ms)</li>



<li>Send offer to peer (network latency)</li>



<li>Peer processes offer (50-100ms)</li>



<li>Peer creates answer (100-200ms)</li>



<li>Peer sets local description (50-100ms)</li>



<li>Answer sent back (network latency)</li>



<li>Set remote description (50-100ms)</li>
</ol>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// Slower due to network round trips
console.time('renegotiate')
const offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
await sendOfferToPeer(offer)
const answer = await waitForAnswer()
await peerA.setRemoteDescription(answer)
console.timeEnd('renegotiate')  // ~500-2000ms

</pre></div>


<h2 class="wp-block-heading" id="decision-tree">Decision Tree</h2>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
Do you need to add a NEW type of media?
├─ YES → Renegotiate (add transceiver, then offer/answer)
└─ NO
   │
   Do you need to change transceiver direction?
   ├─ YES → Renegotiate (change direction, then offer/answer)
   └─ NO
      │
      Do you need to permanently stop a transceiver?
      ├─ YES → Renegotiate (stop transceiver, then offer/answer)
      └─ NO
         │
         Just changing/toggling existing track?
         └─ YES → Use replaceTrack() 

</pre></div>


<h2 class="wp-block-heading" id="summary">Summary</h2>



<p><strong>The Golden Rule</strong>: If the transceiver slot exists, use&nbsp;<code>replaceTrack()</code>. If you need a new slot (or to modify slot properties), renegotiate.</p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/webrtc-renegotiation-when-you-need-it-and-when-you-dont/">WebRTC Renegotiation: When You Need It and When You Don&#8217;t</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating from Cloudflare R2 to Garage in .NET</title>
		<link>https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=migrating-from-cloudflare-r2-to-garage-in-net</link>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Sun, 21 Dec 2025 05:16:15 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1820</guid>

					<description><![CDATA[<p>I recently migrated my .NET side project&#8217;s object storage from Cloudflare R2 to Garage (self-hosted S3-compatible storage). The main driver was cost predictability &#8211; I wanted fixed infrastructure costs instead of variable charges. Here&#8217;s how I configured it. Garage Bucket-to-Website Garage doesn&#8217;t currently support anonymous S3 access, so I used the bucket-to-website feature for public file access. This works by: 1. Setting the bucket name to match my public domain (e.g., cdn.myapp.com) 2. Enabling website mode via the Garage web UI 3. Pointing my DNS to Garage&#8217;s web endpoint 4. Uploading files via S3 API 5. Accessing files directly via </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/">Migrating from Cloudflare R2 to Garage in .NET</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I recently migrated my .NET side project&#8217;s object storage from Cloudflare R2 to Garage (self-hosted S3-compatible storage). The main driver was <strong>cost predictability</strong> &#8211; I wanted fixed infrastructure costs instead of variable charges.</p>



<p>Here&#8217;s how I configured it.</p>



<p><strong>Garage Bucket-to-Website</strong></p>



<p>Garage <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/263">doesn&#8217;t currently support anonymous S3 access</a>, so I used the <strong>bucket-to-website</strong> feature for public file access. This works by:</p>



<p>1. Setting the bucket name to match my public domain (e.g., <strong>cdn.myapp.com</strong>)</p>



<p>2. Enabling website mode via the Garage web UI</p>



<p>3. Pointing my DNS to Garage&#8217;s web endpoint</p>



<p>4. Uploading files via S3 API</p>



<p>5. Accessing files directly via public HTTP URLs on my domain</p>



<p>This provides <strong>persistent public access</strong> without pre-signed URLs &#8211; perfect for truly public assets like logos.</p>



<p><strong>Environment Variables</strong></p>



<p><strong>Before (Cloudflare R2):</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
CLOUDFLARE_R2_ENDPOINT

CLOUDFLARE_R2_ACCESS_KEY_ID

CLOUDFLARE_R2_SECRET_ACCESS_KEY

CLOUDFLARE_R2_BUCKET_NAME
</pre></div>


<p><strong>After (Garage):</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
GARAGE_S3_ENDPOINT=https://cdn.myapp.com # Public web endpoint (matches bucket name)

GARAGE_S3_API_ENDPOINT=https://s3-api.myapp.com # S3 API endpoint (optional)

GARAGE_S3_ACCESS_KEY_ID=your_key_id

GARAGE_S3_SECRET_ACCESS_KEY=your_secret

GARAGE_S3_BUCKET_NAME=cdn.myapp.com # Must match public domain for website mode
</pre></div>


<p></p>



<p><strong>Important:</strong> The bucket name must match your public domain for bucket-to-website to work.</p>



<p><strong>S3 Client Configuration</strong></p>



<p>The critical configuration for Garage compatibility:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var config = new AmazonS3Config

{

   ServiceURL = apiServiceUrl, // Garage S3 API endpoint

   ForcePathStyle = true, // Required for Garage

   AuthenticationRegion = &quot;garage&quot;

};

var s3Client = new AmazonS3Client(accessKeyId, secretAccessKey, config);
</pre></div>


<p><strong>Key points:</strong></p>



<p>&#8211; <strong>ForcePathStyle = true</strong> is <strong>required</strong> for Garage</p>



<p>&#8211; <strong>AuthenticationRegion = &#8220;garage&#8221;</strong> This is particularly important for non-AWS S3-compatible services like Garage that use custom region naming</p>



<p>&#8211; Separate API endpoint for uploads vs. public website endpoint for downloads</p>



<p><strong>Upload Implementation</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public async Task&lt;string&gt; UploadImageAsync(Stream fileStream, string fileName)

{

   var uniqueFileName = $&quot;logos/{Guid.NewGuid()}{Path.GetExtension(fileName)}&quot;;

   var putRequest = new PutObjectRequest

   {

      BucketName = _bucketName,

      Key = uniqueFileName,

      InputStream = fileStream,

      ContentType = contentType,

      UseChunkEncoding = false // Important for compatibility

   };

   await _s3Client.PutObjectAsync(putRequest);

   // Return public website URL

   return $&quot;{_publicEndpoint}/{uniqueFileName}&quot;;

}
</pre></div>


<p>The returned URL (<strong>https://cdn.myapp.com/logos/abc123.png</strong>) is directly accessible &#8211; no auth needed.</p>



<p><strong>Migrating Existing Assets</strong></p>



<p>I used a simple script to transfer existing files from R2 to Garage. The script downloaded files from R2 and re-uploaded them to Garage using the S3 API, preserving the same file paths for zero downtime.</p>



<p><strong>Key Takeaways</strong></p>



<p><strong>Critical configuration:</strong></p>



<p>&#8211; <strong>ForcePathStyle = true</strong> and <strong>AuthenticationRegion = &#8220;garage&#8221;</strong> are essential</p>



<p>&#8211; <strong>UseChunkEncoding = false</strong> prevents upload issues</p>



<p>&#8211; Bucket name <strong>must match</strong> your public domain for website mode</p>



<p>&#8211; DNS must point to Garage&#8217;s web endpoint</p>



<p><strong>Why bucket-to-website:</strong></p>



<p>&#8211; Garage doesn&#8217;t support anonymous S3 access yet</p>



<p>&#8211; Provides persistent public access without pre-signed URLs</p>



<p>&#8211; Perfect for truly public assets (logos, images, etc.)</p>



<p><strong>Why Garage:</strong></p>



<p>&#8211; <strong>Fixed costs</strong> instead of variable API/bandwidth charges</p>



<p>&#8211; <strong>Full control</strong> over storage infrastructure</p>



<p>&#8211; <strong>S3 is compatible</strong> with minimal code changes</p>



<p>For side projects where cost predictability matters, Garage is excellent. Self-hosting means I know exactly what I&#8217;m paying each month.</p>



<p>&#8212;</p>



<p><strong>Resources:</strong></p>



<p>&#8211; <a href="https://garagehq.deuxfleurs.fr/">Garage Documentation</a></p>



<p>&#8211; <a href="https://www.nuget.org/packages/AWSSDK.S3/">AWSSDK.S3 NuGet Package</a></p>



<p></p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/migrating-from-cloudflare-r2-to-garage-in-net/">Migrating from Cloudflare R2 to Garage in .NET</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Implementing the MCP Stdio Transport Layer in .NET for VSCode Integration</title>
		<link>https://swacblooms.com/implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration</link>
					<comments>https://swacblooms.com/implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration/#comments</comments>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Tue, 03 Jun 2025 08:09:56 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1798</guid>

					<description><![CDATA[<p>Hi &#x1f44b;&#x1f3fe;, in this post, I will discuss building an MCP tool for VSCode using .NET or any client that supports MCP. MCP stands for Model Context Protocol. It is a standard that outlines how large language models can interact with tools, commands or function execution to provide more context to them. How MCP Works in VSCode Visual Studio Code serves as the host and manages launching the MCP clients. The MCP client interacts with LLMs and understands how to interact with MCP servers to invoke tools or add more context to its interaction with LLMS. In this case, the </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration/">Implementing the MCP Stdio Transport Layer in .NET for VSCode Integration</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hi <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b-1f3fe.png" alt="👋🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" />, in this post, I will discuss building an MCP tool for VSCode using .NET or any client that supports MCP. </p>



<p>MCP stands for Model Context Protocol. It is a standard that outlines how large language models can interact with tools, commands or function execution to provide more context to them.</p>



<h2 class="wp-block-heading">How MCP Works in VSCode</h2>



<p>Visual Studio Code serves as the host and manages launching the MCP clients. </p>



<p>The MCP client interacts with LLMs and understands how to interact with MCP servers to invoke tools or add more context to its interaction with LLMS. In this case, the client is GitHub Copilot Agent.</p>



<p>The MCP server can be implemented either by hosting a literal web server or by using a standard input and output CLI tool(npm, docker etc) that can interact using the MCP standard</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Component</td><td>Role in MCP</td><td>Example</td></tr><tr><td>VSCode</td><td>Host</td><td>Provides context, manages agents</td></tr><tr><td>GitHub Copilot Agent</td><td>MCP Client</td><td>Implements the MCP Agent</td></tr><tr><td>DotNet Console App</td><td>MCP Server</td><td>Acts like a server listening to stdin/stdout</td></tr></tbody></table></figure>



<p>MCP protocol uses <a href="https://www.jsonrpc.org/specification">JSON-RPC</a> to communicate. The JSON I&#8217;ll be posting below will be formatted, but the standard requires that the JSON is void of a new line (&#8220;\n&#8221;).</p>



<p>The protocol also requires that responses return the same ID from the request.</p>



<p>I tried implementing a partial MCP server that allows execution of MCP tools and doesn&#8217;t support features like sampling, batching, logging or sending errors.</p>



<p>What the tool does is that it returns a random quote based on the hour of the day. All communication between the server and client is through the standard input and output stream.</p>



<p>I used VSCode Insider, since the docs said this feature is still in preview. This is what the flow looks like.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="637" src="https://swacblooms.com/wp-content/uploads/2025/06/image-1-1024x637.png" alt="" class="wp-image-1802" srcset="https://swacblooms.com/wp-content/uploads/2025/06/image-1-1024x637.png 1024w, https://swacblooms.com/wp-content/uploads/2025/06/image-1-300x187.png 300w, https://swacblooms.com/wp-content/uploads/2025/06/image-1-768x478.png 768w, https://swacblooms.com/wp-content/uploads/2025/06/image-1-1536x956.png 1536w, https://swacblooms.com/wp-content/uploads/2025/06/image-1.png 2021w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>The Client (GitHub Copilot) sends an initialisation request that looks like this</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 1,
  &quot;method&quot;: &quot;initialize&quot;,
  &quot;params&quot;: {
    &quot;protocolVersion&quot;: &quot;2025-03-26&quot;,
    &quot;capabilities&quot;: {
      &quot;roots&quot;: {
        &quot;listChanged&quot;: true
      },
      &quot;sampling&quot;: {}
    },
    &quot;clientInfo&quot;: {
      &quot;name&quot;: &quot;Visual Studio Code - Insiders&quot;,
      &quot;version&quot;: &quot;1.101.0-insider&quot;
    }
  }
}
</pre></div>


<ul class="wp-block-list">
<li>Then my MCP server replies with this </li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 1,
  &quot;result&quot;: {
    &quot;protocolVersion&quot;: &quot;2024-11-05&quot;,
    &quot;capabilities&quot;: {
      &quot;tools&quot;: {
        &quot;listChanged&quot;: true
      }
    },
    &quot;serverInfo&quot;: {
      &quot;name&quot;: &quot;ExampleServer&quot;,
      &quot;version&quot;: &quot;1.0.0&quot;
    },
    &quot;instructions&quot;: &quot;Helps returning quotes based on the time of the day.&quot;
  }
}
</pre></div>


<ul class="wp-block-list">
<li>Then the Client (GitHub Copilot Agent) sends a notification</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;method&quot;: &quot;notifications/initialized&quot;,
  &quot;jsonrpc&quot;: &quot;2.0&quot;
}
</pre></div>


<p>In my implementation, I didn&#8217;t handle this notification. Notifications in the MCP protocol are one-way messages.</p>



<ul class="wp-block-list">
<li>The client then makes a request to my server asking it to list the available tools and their required arguments.</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 2,
  &quot;method&quot;: &quot;tools/list&quot;,
  &quot;params&quot;: {}
}
</pre></div>


<ul class="wp-block-list">
<li>Then my server returns a list of tools using the MCP spec. I attached attributes to the methods that I wanted to expose, and then I used reflection to dynamically generate the data.</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 2,
  &quot;result&quot;: {
    &quot;tools&quot;: &#91;
      {
        &quot;name&quot;: &quot;get_time_based_quotes&quot;,
        &quot;description&quot;: &quot;Get a quote based on the hour of day&quot;,
        &quot;inputSchema&quot;: {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: {
            &quot;hour&quot;: {
              &quot;type&quot;: &quot;integer&quot;,
              &quot;description&quot;: &quot;Parameter hour&quot;
            }
          },
          &quot;required&quot;: &#91;
            &quot;hour&quot;
          ]
        }
      },
      {
        &quot;name&quot;: &quot;say_hello&quot;,
        &quot;description&quot;: &quot;Say hello to someone&quot;,
        &quot;inputSchema&quot;: {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: {
            &quot;name&quot;: {
              &quot;type&quot;: &quot;string&quot;,
              &quot;description&quot;: &quot;Parameter name&quot;
            }
          },
          &quot;required&quot;: &#91;
            &quot;name&quot;
          ]
        }
      }
    ]
  }
}
</pre></div>


<ul class="wp-block-list">
<li>When I write a prompt that invokes my tool from GitHub Copilot chat, I get this message</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 3,
  &quot;method&quot;: &quot;tools/call&quot;,
  &quot;params&quot;: {
    &quot;name&quot;: &quot;get_time_based_quotes_based&quot;,
    &quot;arguments&quot;: {
      &quot;hour&quot;: 7
    },
    &quot;_meta&quot;: {
      &quot;progressToken&quot;: &quot;bc9350cd-4d19-44ec-ac12-2056f11983a4&quot;
    }
  }
}
</pre></div>


<ul class="wp-block-list">
<li>Then my MCP server returns this</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 3,
  &quot;result&quot;: {
    &quot;content&quot;: &#91;
      {
        &quot;type&quot;: &quot;text&quot;,
        &quot;text&quot;: &quot;Every morning is a new beginning. Take a deep breath and start again.&quot;
      }
    ]
  }
}
</pre></div>


<p>The video below shows all this in VS Code.</p>



<figure class="wp-block-video"><video controls src="https://swacblooms.com/wp-content/uploads/2025/06/Screen-Recording-2025-06-03-at-07.45.17.mov"></video></figure>



<p>From the video above, you can see that I am using the new <strong>dotnet run app.cs</strong> new feature that is still in preview <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>To learn more about MCP, check out its <a href="https://modelcontextprotocol.io/specification/2025-03-26">specifications</a>. And to learn more about the dotnet run app.cs feature, check out their <a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/#what-is-dotnet-run-app.cs">blog post</a>.</p>



<p>My basic MCP server implementation can be seen <a href="https://github.com/sammychinedu2ky/MCPServer">here</a></p>



<p>Thanks for reading and bye <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b-1f3fe.png" alt="👋🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p></p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration/">Implementing the MCP Stdio Transport Layer in .NET for VSCode Integration</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://swacblooms.com/implementing-the-mcp-stdio-transport-layer-in-net-for-vscode-integration/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		<enclosure length="18793927" type="video/quicktime" url="https://swacblooms.com/wp-content/uploads/2025/06/Screen-Recording-2025-06-03-at-07.45.17.mov"/>

			<itunes:explicit/><itunes:subtitle>Hi &amp;#x1f44b;&amp;#x1f3fe;, in this post, I will discuss building an MCP tool for VSCode using .NET or any client that supports MCP. MCP stands for Model Context Protocol. It is a standard that outlines how large language models can interact with tools, commands or function execution to provide more context to them. How MCP Works in VSCode Visual Studio Code serves as the host and manages launching the MCP clients. The MCP client interacts with LLMs and understands how to interact with MCP servers to invoke tools or add more context to its interaction with LLMS. In this case, the The post Implementing the MCP Stdio Transport Layer in .NET for VSCode Integration appeared first on Swacblooms&amp;#x1f98b;.</itunes:subtitle><itunes:summary>Hi &amp;#x1f44b;&amp;#x1f3fe;, in this post, I will discuss building an MCP tool for VSCode using .NET or any client that supports MCP. MCP stands for Model Context Protocol. It is a standard that outlines how large language models can interact with tools, commands or function execution to provide more context to them. How MCP Works in VSCode Visual Studio Code serves as the host and manages launching the MCP clients. The MCP client interacts with LLMs and understands how to interact with MCP servers to invoke tools or add more context to its interaction with LLMS. In this case, the The post Implementing the MCP Stdio Transport Layer in .NET for VSCode Integration appeared first on Swacblooms&amp;#x1f98b;.</itunes:summary><itunes:keywords>Uncategorized</itunes:keywords></item>
		<item>
		<title>Running Entity Framework Migrations in an Aspire-Bootstrapped Orleans Project</title>
		<link>https://swacblooms.com/running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project</link>
					<comments>https://swacblooms.com/running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project/#comments</comments>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 10 Mar 2025 08:28:56 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1789</guid>

					<description><![CDATA[<p>Hi guys &#x1f44b;&#x1f3fe;, so you know that Aspire Aspire Aspire has been the buzz in the DotNet world, and it does simplify setting up your development environment. I tried bootstrapping an ASP.NET cohosted Orleans project using the Orleans and the PostgreSQL integration. When I tried to add a new migration, errors were thrown because the Orleans integration required some environment variables to be available. Those variables are only available and injected when running your project through the Aspire AppHost project. The code below is a snippet of the initial state of the code that resorted to errors when I tried </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project/">Running Entity Framework Migrations in an Aspire-Bootstrapped Orleans Project</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hi guys <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b-1f3fe.png" alt="👋🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" />, so you know that Aspire Aspire Aspire has been the buzz in the DotNet world, and it does simplify setting up your development environment. </p>



<p>I tried bootstrapping an ASP.NET cohosted Orleans project using the Orleans and the PostgreSQL integration. When I tried to add a new migration, errors were thrown because the Orleans integration required some environment variables to be available. Those variables are only available and injected when running your project through the Aspire AppHost project.</p>



<p>The code below is a snippet of the initial state of the code that resorted to errors when I tried to run my entity framework migrations.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
builder.Services.AddDbContextPool&lt;AppDbContext&gt;(options =&gt;
{
    options.UseNpgsql(builder.Configuration.GetConnectionString(&quot;postgresdb&quot;)
        ,npgsqlOptionsAction: handleDbRetry()).UseExceptionProcessor();
});
builder.AddKeyedRedisClient(&quot;redis&quot;);
builder.UseOrleans(siloBuilder =&gt;
{
      siloBuilder.Services.AddPooledDbContextFactory&lt;AppDbContext&gt;(options =&gt;
      {
           options.UseNpgsql(builder.Configuration.GetConnectionString(&quot;postgresdb&quot;),
               npgsqlOptionsAction: handleDbRetry());
      });
 });
</pre></div>


<p>When I execute <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">dotnet ef migrations add [name of migration]</mark></em>, the error snippet below is thrown</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
An error occurred while accessing the Microsoft.Extensions.Hosting services. Continuing without the application service provider. Error: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Orleans.Runtime.MembershipService.MembershipTableManager Lifetime: Singleton ImplementationType: Orleans.Runtime.MembershipService.MembershipTableManager':
</pre></div>


<p>To prevent this, I resorted to always commenting out the <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">builder.UseOrleans</mark></em> block when I want to run migrations. And it worked but I couldn&#8217;t see myself always doing this moving forward. So I did some research and discovered that the migration command doesn&#8217;t inject any special environment variable when trying to add a migration.</p>



<p>I also discovered that you could add an environment variable which could be analysed before the <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">builder.UseOrleans</mark></em> block runs. This works but I didn&#8217;t want to always add extra commands aside from executing the normal <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">dotnet ef migration add </mark></em>command.</p>



<p>Finally, I found a way on Stackoverflow to detect if the <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">dotnet ef migrations add</mark></em> command is executed <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>



<p>All I needed to do was check if a particular letter combination was available in the executable file name or command line arguments tied to the <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">dotnet ef migrations add</mark></em> process. The <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">Environment.GetCommandLineArgs() </mark></em>method allows you to retrieve this and perform desired checks.</p>



<p>My final code block that does this seamlessly with the same <em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">dotnet ef migrations add</mark></em> command can be seen below.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var isMigrations = Environment.GetCommandLineArgs()&#91;0].Contains(&quot;ef.dll&quot;);
builder.Services.AddDbContextPool&lt;AppDbContext&gt;(options =&gt;
{
    options.UseNpgsql(builder.Configuration.GetConnectionString(&quot;postgresdb&quot;)
        ,npgsqlOptionsAction: handleDbRetry()).UseExceptionProcessor();
});
builder.AddKeyedRedisClient(&quot;redis&quot;);
if (!isMigrations)
{
    builder.UseOrleans(siloBuilder =&gt;
    {
        siloBuilder.Services.AddPooledDbContextFactory&lt;AppDbContext&gt;(options =&gt;
        {
            options.UseNpgsql(builder.Configuration.GetConnectionString(&quot;postgresdb&quot;),
                npgsqlOptionsAction: handleDbRetry());
        });
    });
}
</pre></div>


<p>I just needed to check if the executable file name contained (&#8220;ef.dll&#8221;), if it did then that meant I was running a migration and there was no need to configure or set up the Orleans silo.</p>



<p>Thanks for reading. Bye <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/270c-1f3fe.png" alt="✌🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project/">Running Entity Framework Migrations in an Aspire-Bootstrapped Orleans Project</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://swacblooms.com/running-entity-framework-migrations-in-an-aspire-bootstrapped-orleans-project/feed/</wfw:commentRss>
			<slash:comments>63</slash:comments>
		
		
			</item>
		<item>
		<title>Using XDebug in Laravel Sail (VSCODE)</title>
		<link>https://swacblooms.com/using-xdebug-in-laravel-sail-vscode/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=using-xdebug-in-laravel-sail-vscode</link>
					<comments>https://swacblooms.com/using-xdebug-in-laravel-sail-vscode/#comments</comments>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Mon, 20 Jan 2025 07:50:52 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1766</guid>

					<description><![CDATA[<p>Hi guys &#x1f44b;&#x1f3fe;, in this article I will be writing about how I got to set up XDebug to work in my Visual Studio Code editor for both my testing environment through the terminal and for browser request. For someone pampered with the luxury of having a seamless debugging experience in Visual Studio IDE for C#, I also wanted to enjoy that comfort in a Laravel sail project setup. This led me to some exploration and luckily I found a light at the end of the tunnel. Requirements Browser Debugging The mapping enables the debugger to locate the right files </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/using-xdebug-in-laravel-sail-vscode/">Using XDebug in Laravel Sail (VSCODE)</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hi guys <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b-1f3fe.png" alt="👋🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" />, in this article I will be writing about how I got to set up XDebug to work in my Visual Studio Code editor for both my testing environment through the terminal and for browser request.</p>



<p>For someone pampered with the luxury of having a seamless debugging experience in Visual Studio IDE for C#, I also wanted to enjoy that comfort in a Laravel sail project setup. This led me to some exploration and luckily I found a light at the end of the tunnel.</p>



<h2 class="wp-block-heading">Requirements</h2>



<ul class="wp-block-list">
<li>Visual Studio Code</li>



<li>PHP Debug Extension (<a href="https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug" target="_blank" rel="noreferrer noopener">here</a>)</li>



<li>PHP and Composer</li>



<li>XDebug Browser Extension(<a href="https://xdebug.org/docs/step_debug#browser-extensions" target="_blank" rel="noreferrer noopener">here</a>)</li>
</ul>



<h2 class="wp-block-heading">Browser Debugging</h2>



<ul class="wp-block-list">
<li>I created a Laravel project setup called &#8220;<strong>mysail</strong>&#8220;</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
           composer create-project laravel/laravel mysail
</pre></div>


<ul class="wp-block-list">
<li>I navigated to the newly created project </li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
          cd mysail
</pre></div>


<ul class="wp-block-list">
<li>I installed sail </li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
           php artisan sail:install
</pre></div>


<ul class="wp-block-list">
<li>I added this environment variable to activate XDEBUG for debugging sessions</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
           SAIL_XDEBUG_MODE=develop,debug,coverage
</pre></div>


<ul class="wp-block-list">
<li>I started my sail environment </li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
./vendor/bin/sail up
</pre></div>


<ul class="wp-block-list">
<li>I opened a new terminal and ran my migrations</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
./vendor/bin/sail artisan migrate
</pre></div>


<ul class="wp-block-list">
<li>I clicked on the &#8220;<strong>Run and Debug</strong>&#8221; option in VSCode</li>



<li>I clicked on the &#8220;<strong>create a launch.json file</strong>&#8220;</li>
</ul>



<figure class="wp-block-image size-full"><img decoding="async" width="767" height="504" src="https://swacblooms.com/wp-content/uploads/2025/01/image.png" alt="" class="wp-image-1768" srcset="https://swacblooms.com/wp-content/uploads/2025/01/image.png 767w, https://swacblooms.com/wp-content/uploads/2025/01/image-300x197.png 300w, https://swacblooms.com/wp-content/uploads/2025/01/image-350x230.png 350w" sizes="(max-width: 767px) 100vw, 767px" /></figure>



<ul class="wp-block-list">
<li>I clicked on the &#8220;<strong>PHP(XDebug)</strong>&#8221; option which opened some pre-configured debugging setup</li>



<li>I looked for the one that had &#8220;Listen for Xdebug&#8221; and changed it from:</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
        {
            &quot;name&quot;: &quot;Listen for Xdebug&quot;,
            &quot;type&quot;: &quot;php&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;port&quot;: 9003
        },

     to this


        {
            &quot;name&quot;: &quot;Listen for Xdebug&quot;,
            &quot;type&quot;: &quot;php&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;port&quot;: 9003,
            &quot;pathMappings&quot;: {
                &quot;/var/www/html&quot;: &quot;${workspaceRoot}&quot;
            }
        },
</pre></div>


<p>The mapping enables the debugger to locate the right files to monitor set breakpoints.</p>



<ul class="wp-block-list">
<li>In my browser, I activated the XDebug browser extension by clicking on the &#8220;Debug&#8221; option of the extension</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="429" height="264" src="https://swacblooms.com/wp-content/uploads/2025/01/image-4.png" alt="" class="wp-image-1780" srcset="https://swacblooms.com/wp-content/uploads/2025/01/image-4.png 429w, https://swacblooms.com/wp-content/uploads/2025/01/image-4-300x185.png 300w" sizes="(max-width: 429px) 100vw, 429px" /></figure></div>


<ul class="wp-block-list">
<li>I added a breakpoint to the home route to test if it works</li>



<li>I clicked on the &#8220;<strong>Listen for Xdebug</strong>&#8221; button to activate the debugging session</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="295" src="https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320-1024x295.png" alt="" class="wp-image-1769" srcset="https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320-1024x295.png 1024w, https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320-300x86.png 300w, https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320-768x221.png 768w, https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320-1536x442.png 1536w, https://swacblooms.com/wp-content/uploads/2025/01/screenshot-1737357744320.png 1859w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>Now navigating to the website URL (localhost) stops exactly at line 6</li>
</ul>



<ul class="wp-block-list">
<li>Yay!! it works, next I wanted to ensure I could use it to debug my test.</li>



<li>I tried using the default scaffolded test created in a new Laravel project</li>



<li>I added a breakpoint here:</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="565" src="https://swacblooms.com/wp-content/uploads/2025/01/image-1-1024x565.png" alt="" class="wp-image-1770" srcset="https://swacblooms.com/wp-content/uploads/2025/01/image-1-1024x565.png 1024w, https://swacblooms.com/wp-content/uploads/2025/01/image-1-300x165.png 300w, https://swacblooms.com/wp-content/uploads/2025/01/image-1-768x423.png 768w, https://swacblooms.com/wp-content/uploads/2025/01/image-1.png 1449w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>I opened a new terminal and ran this &#8220;<strong>./vendor/bin/sail debug test &#8211;filter test_the_application_returns_a_successful_response</strong>&#8221; instead of &#8220;<strong>./vendor/bin/sail artisan test &#8211;filter test_the_application_returns_a_successful_response</strong>&#8221; </li>



<li>Note the difference in replacing &#8220;<strong>artisan</strong>&#8221; with &#8220;<strong>debug</strong>&#8220;</li>



<li>And that worked as well</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="343" src="https://swacblooms.com/wp-content/uploads/2025/01/image-2-1024x343.png" alt="" class="wp-image-1771" srcset="https://swacblooms.com/wp-content/uploads/2025/01/image-2-1024x343.png 1024w, https://swacblooms.com/wp-content/uploads/2025/01/image-2-300x101.png 300w, https://swacblooms.com/wp-content/uploads/2025/01/image-2-768x257.png 768w, https://swacblooms.com/wp-content/uploads/2025/01/image-2-1536x515.png 1536w, https://swacblooms.com/wp-content/uploads/2025/01/image-2.png 1662w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Things to note:</h2>



<ul class="wp-block-list">
<li>Set your env: <strong>SAIL_XDEBUG_MODE=debug,debug,coverage</strong><span style="color: initial; font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, Oxygen-Sans, Ubuntu, Cantarell, &quot;Helvetica Neue&quot;, sans-serif;"> before running &#8220;</span><strong style="color: initial; font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, Oxygen-Sans, Ubuntu, Cantarell, &quot;Helvetica Neue&quot;, sans-serif;">sail up</strong><span style="color: initial; font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, Oxygen-Sans, Ubuntu, Cantarell, &quot;Helvetica Neue&quot;, sans-serif;">&#8220;</span></li>



<li>Add the path mapping to your PHP debugger VSCode configuration</li>



<li>Activate the XDebug browser extension by clicking the &#8220;Debug&#8221; option when debugging browser requests.</li>



<li>For your terminal debugging activities replace the &#8220;artisan&#8221; command with the word &#8220;debug&#8221;</li>
</ul>



<p>Thanks for reading and bye <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b-1f3fe.png" alt="👋🏾" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/using-xdebug-in-laravel-sail-vscode/">Using XDebug in Laravel Sail (VSCODE)</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://swacblooms.com/using-xdebug-in-laravel-sail-vscode/feed/</wfw:commentRss>
			<slash:comments>36</slash:comments>
		
		
			</item>
		<item>
		<title>Custom Redis Streams Provider for Orleans</title>
		<link>https://swacblooms.com/custom-redis-streams-provider-for-orleans/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=custom-redis-streams-provider-for-orleans</link>
					<comments>https://swacblooms.com/custom-redis-streams-provider-for-orleans/#comments</comments>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Thu, 27 Jun 2024 08:43:12 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1741</guid>

					<description><![CDATA[<p>Hi &#x1f44b;, so in today&#8217;s post I will be narrating how I wrote a custom Redis streams provider for Orleans. Before I continue, I want to express my gratitude to the Orleans community for their support in addressing my queries. Special thanks to Ledjon Behluli and Reuben Bond for their invaluable contributions to the community. Streams in Orleans enable your grains to react to a sequence of events. It allows the decoupling of the data generator from the data consumer. Orleans&#8217; extensibility allows developers to create custom stream providers to enable interoperability with the Orleans runtime. Redis offers two popular </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/custom-redis-streams-provider-for-orleans/">Custom Redis Streams Provider for Orleans</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hi <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" />, so in today&#8217;s post I will be narrating how I wrote a custom Redis streams provider for Orleans.</p>



<p>Before I continue, I want to express my gratitude to the Orleans community for their support in addressing my queries. Special thanks to Ledjon Behluli and Reuben Bond for their invaluable contributions to the community.</p>



<p>Streams in Orleans enable your grains to react to a sequence of events. It allows the decoupling of the data generator from the data consumer.</p>



<p>Orleans&#8217; extensibility allows developers to create custom stream providers to enable interoperability with the Orleans runtime.</p>



<p>Redis offers two popular messaging and real-time event-driven communication. They are:</p>



<ul class="wp-block-list">
<li>Redis Pub/Sub</li>



<li>Redis Streams</li>
</ul>



<p>I chose to write a Redis stream provider for Orleans due to the benefits Redis Stream has over Redis Pub/Sub. Redis stream supports message persistence, it also supports multiple consumers reading events at their own pace and other complex message processing.</p>



<p>For this article and project, I will be using Redis stream as a queue and a message broker.</p>



<p>To learn about streams in Orleans check these resources:</p>



<ul class="wp-block-list">
<li><a href="https://learn.microsoft.com/en-us/dotnet/orleans/streaming/?pivots=orleans-7-0" target="_blank" rel="noreferrer noopener">Getting Started with Streams In Orleans</a></li>



<li><a href="https://learn.microsoft.com/en-us/dotnet/orleans/implementation/streams-implementation/?WT.mc_id=DT-MVP-5004830" target="_blank" rel="noreferrer noopener">Streams Implementation in Orleans</a></li>
</ul>



<p>To learn about Redis Streams check <a href="https://redis.io/docs/latest/develop/data-types/streams/" target="_blank" rel="noreferrer noopener">here</a>.</p>



<h2 class="wp-block-heading">Building the Redis Streams Provider</h2>



<p>Building a custom stream provider for Orleans requires implementing some interfaces. </p>



<p>The <strong>IQueueAdapterFactory</strong> interface is used for bootstrapping the components needed for a stream provider to be compatible with the Orleans runtime.</p>



<p>The <strong>IQueueAdapter</strong> interface enables the producer to send an event to the right queue and it also enables the Orleans runtime to know which queues to retrieve the events from before sending it to the consumer(Grain).</p>



<p>In Orleans each queue is assigned a pulling agent, the pulling agent retrieves a message from an assigned queue and sends it to a consumer which is a target Grain in Orleans.</p>



<p>You can decide to set the number of queues to one or more from your queue mapping settings when building the Orleans client and server host.</p>



<p>Orleans streams give you the freedom to use more than one queue to support various scalability, reliability and performance needs in a distributed system.</p>



<p>The <strong>IStreamFailureHandler</strong> interface enables you to define how you want to handle delivery errors to stream subscriptions.</p>



<p>The <strong>IQueueAdapterCache</strong> interface enables you to define how you want to handle the caching of the retrieved events from the queue.</p>



<p>The <strong>IStreamQueueMapper</strong> interface enables the stream/event producer to know which queue to push an event to by mapping the stream ID to a queue and it enables the runtime to know how many queues and cache to generate for the pulling agent.</p>



<p>Below is my implementation for the <strong><strong>IQueueAdapterFactory</strong> </strong>interface:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RedisStreamFactory : IQueueAdapterFactory
{
    private readonly IDatabase _database;
    private readonly ILoggerFactory _loggerFactory;
    private readonly string _providerName;
    private readonly IStreamFailureHandler _streamFailureHandler;
    private readonly SimpleQueueCacheOptions _simpleQueueCacheOptions;
    private readonly HashRingBasedStreamQueueMapper _hashRingBasedStreamQueueMapper;

    public RedisStreamFactory(IDatabase database,
        ILoggerFactory loggerFactory,
        string providerName,
        IStreamFailureHandler streamFailureHandler,
        SimpleQueueCacheOptions simpleQueueCacheOptions,
        HashRingStreamQueueMapperOptions hashRingStreamQueueMapperOptions
        )
    {
        _database = database;
        _loggerFactory = loggerFactory;
        _providerName = providerName;
        _streamFailureHandler = streamFailureHandler;
        _simpleQueueCacheOptions = simpleQueueCacheOptions;
        _hashRingBasedStreamQueueMapper = new HashRingBasedStreamQueueMapper(hashRingStreamQueueMapperOptions, providerName);
    }

    public static IQueueAdapterFactory Create(IServiceProvider provider, string providerName)
    {
        var database = provider.GetRequiredService&lt;IDatabase&gt;();
        var loggerFactory = provider.GetRequiredService&lt;ILoggerFactory&gt;();
        var simpleQueueCacheOptions = provider.GetOptionsByName&lt;SimpleQueueCacheOptions&gt;(providerName);
        var hashRingStreamQueueMapperOptions = provider.GetOptionsByName&lt;HashRingStreamQueueMapperOptions&gt;(providerName);
        var streamFailureHandler = new RedisStreamFailureHandler(loggerFactory.CreateLogger&lt;RedisStreamFailureHandler&gt;());
        return new RedisStreamFactory(database, loggerFactory, providerName, streamFailureHandler, simpleQueueCacheOptions, hashRingStreamQueueMapperOptions);

    }

    public Task&lt;IQueueAdapter&gt; CreateAdapter()
    {
        return Task.FromResult&lt;IQueueAdapter&gt;(new RedisStreamAdapter(_database, _providerName, _hashRingBasedStreamQueueMapper, _loggerFactory));
    }

    public Task&lt;IStreamFailureHandler&gt; GetDeliveryFailureHandler(QueueId queueId)
    {
        return Task.FromResult(_streamFailureHandler);
    }

    public IQueueAdapterCache GetQueueAdapterCache()
    {
        return new SimpleQueueAdapterCache(_simpleQueueCacheOptions, _providerName, _loggerFactory);
    }

    public IStreamQueueMapper GetStreamQueueMapper()
    {
        return _hashRingBasedStreamQueueMapper;
    }
}
</pre></div>


<p>Next is my implementation of the <strong>IQueueAdapter</strong> interface, this interface enables the creation of a queue receiver. A receiver is generated for each queue. This interface is also used to send or push events to the queue. To push an event to the assigned queue, I had to use the inbuilt <strong>HashRingBasedStreamQueueMapper</strong> to obtain the queue a stream is assigned to. But you can write a custom queue mapper that suits your needs.</p>



<p>I also made use of the inbuilt <strong>SimpleQueueAdapterCache</strong> implementation to enable the caching of pulled events. But you can also use a custom one based on your needs.</p>



<p>In my implementation I made a batch of events to be a single event in Redis streams. I made the single event to comprise a stream&#8217;s namespace, a stream key, an event type and the actual data (event data).</p>



<p>This information will enable the queues pulling agent to know the consumer(Grain) for each event. Also do ensure that the <strong>Name</strong> property is properly set to the name of the stream provider.</p>



<p>The code below is my implementation of the interface.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RedisStreamAdapter : IQueueAdapter
{
    private readonly IDatabase _database;
    private readonly string _providerName;
    private readonly HashRingBasedStreamQueueMapper _hashRingBasedStreamQueueMapper;
    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger&lt;RedisStreamAdapter&gt; _logger;

    public RedisStreamAdapter(IDatabase database, string providerName, HashRingBasedStreamQueueMapper hashRingBasedStreamQueueMapper, ILoggerFactory loggerFactory)
    {
        _database = database;
        _providerName = providerName;
        _hashRingBasedStreamQueueMapper = hashRingBasedStreamQueueMapper;
        _loggerFactory = loggerFactory;
        _logger = loggerFactory.CreateLogger&lt;RedisStreamAdapter&gt;();
    }

    public string Name =&gt; _providerName;

    public bool IsRewindable =&gt; false;

    public StreamProviderDirection Direction =&gt; StreamProviderDirection.ReadWrite;

    public IQueueAdapterReceiver CreateReceiver(QueueId queueId)
    {
        return new RedisStreamReceiver(queueId, _database, _loggerFactory.CreateLogger&lt;RedisStreamReceiver&gt;());
    }

    public async Task QueueMessageBatchAsync&lt;T&gt;(StreamId streamId, IEnumerable&lt;T&gt; events, StreamSequenceToken token, Dictionary&lt;string, object&gt; requestContext)
    {
        try
        {
            foreach (var @event in events)
            {
                NameValueEntry streamNamespaceEntry = new(&quot;streamNamespace&quot;, streamId.Namespace);
                NameValueEntry streamKeyEntry = new(&quot;streamKey&quot;, streamId.Key);
                NameValueEntry eventTypeEntry = new(&quot;eventType&quot;, @event!.GetType().Name);
                NameValueEntry dataEntry = new(&quot;data&quot;, JsonSerializer.Serialize(@event));
                var queueId = _hashRingBasedStreamQueueMapper.GetQueueForStream(streamId);
                await _database.StreamAddAsync(queueId.ToString(), &#91;streamNamespaceEntry, streamKeyEntry, eventTypeEntry, dataEntry]);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Error adding event to stream {StreamId}&quot;, streamId);
        }
    }
}
</pre></div>


<p>Next, I implemented the <strong>IQueueAdapterReceiver</strong> interface, this implementation is used by pulling agents to retrieve messages or events from queues( Redis Stream) and sends the messages to the consumers (Grains). It also enables you to perform some cleanups or acknowledgement of processed messages using the <strong>MessagesDeliveredAsync</strong> method.</p>



<p>My implementation can be seen below.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RedisStreamReceiver : IQueueAdapterReceiver
{
    private readonly QueueId _queueId;
    private readonly IDatabase _database;
    private readonly ILogger&lt;RedisStreamReceiver&gt; _logger;
    private string _lastId = &quot;0&quot;;
    private Task? pendingTasks;

    public RedisStreamReceiver(QueueId queueId, IDatabase database, ILogger&lt;RedisStreamReceiver&gt; logger)
    {
        _queueId = queueId;
        _database = database;
        _logger = logger;
    }

    public async Task&lt;IList&lt;IBatchContainer&gt;?&gt; GetQueueMessagesAsync(int maxCount)
    {
        try
        {
            var events = _database.StreamReadGroupAsync(_queueId.ToString(), &quot;consumer&quot;, _queueId.ToString(), _lastId, maxCount);
            pendingTasks = events;
            _lastId = &quot;&gt;&quot;;
            var batches = (await events).Select(e =&gt; new RedisStreamBatchContainer(e)).ToList&lt;IBatchContainer&gt;();
            return batches;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Error reading from stream {QueueId}&quot;, _queueId);
            return default;
        }
        finally
        {
            pendingTasks = null;
        }


    }

    public async Task Initialize(TimeSpan timeout)
    {
        try
        {
            using (var cts = new CancellationTokenSource(timeout))
            {
                var task = _database.StreamCreateConsumerGroupAsync(_queueId.ToString(), &quot;consumer&quot;, &quot;$&quot;, true);
                await task.WaitAsync(timeout, cts.Token);
            }
        }
        catch (Exception ex) when (ex.Message.Contains(&quot;name already exists&quot;)) { }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Error initializing stream {QueueId}&quot;, _queueId);
        }
    }

    public async Task MessagesDeliveredAsync(IList&lt;IBatchContainer&gt; messages)
    {
        try
        {
            foreach (var message in messages)
            {
                var container = message as RedisStreamBatchContainer;
                if (container != null)
                {
                    var ack = _database.StreamAcknowledgeAsync(_queueId.ToString(), &quot;consumer&quot;, container.StreamEntry.Id);
                    pendingTasks = ack;
                    await ack;
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Error acknowledging messages in stream {QueueId}&quot;, _queueId);
        }
        finally
        {
            pendingTasks = null;
        }
    }

    public async Task Shutdown(TimeSpan timeout)
    {
        using (var cts = new CancellationTokenSource(timeout))
        {

            if (pendingTasks is not null)
            {
                await pendingTasks.WaitAsync(timeout, cts.Token);
            }
        }
        _logger.LogInformation(&quot;Shutting down stream {QueueId}&quot;, _queueId);
    }
}
</pre></div>


<p>Next, I created a new type called <strong>RedisStreamSequenceToken</strong> which I inherited from the <strong>StreamSequenceToken</strong> abstract class.</p>



<p>I needed a way to convert Redis streams event ID, into a form that is compatible with Orleans streams. so this inheritance did the magic. I converted the Redis stream ID timestamp section into the  <strong>SequenceNumber </strong>property and the Redis stream sequence number section into the <strong>EventIndex </strong>property.</p>



<p>The inheritance requires using the <strong>GenerateSerializer</strong> attribute to mark the new type. This will enable the Orleans runtime to copy the object within its internals. You also need to ensure that the <strong>Microsoft.Orleans.Sdk</strong> is used to enable the proper generation of the serialized code.</p>



<p>The code below is my implementation:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RedisStreamSequenceToken : StreamSequenceToken
{
    &#91;Id(0)]
    public override long SequenceNumber { get; protected set; }
    &#91;Id(1)]
    public override int EventIndex { get; protected set; }

    public RedisStreamSequenceToken(RedisValue id)
    {

        var split = id.ToString().Split(&quot;-&quot;);
        SequenceNumber = long.Parse(split&#91;0]);
        EventIndex = int.Parse(split&#91;1]);
    }
    public RedisStreamSequenceToken(long sequenceNumber, int eventIndex)
    {
        SequenceNumber = sequenceNumber;
        EventIndex = eventIndex;
    }
    public override int CompareTo(StreamSequenceToken other)
    {
        if (other is null) throw new ArgumentNullException(nameof(other));
        if (other is RedisStreamSequenceToken token)
        {
            if (SequenceNumber == token.SequenceNumber)
            {
                return EventIndex.CompareTo(token.EventIndex);
            }
            return SequenceNumber.CompareTo(token.SequenceNumber);
        }
        throw new ArgumentException(&quot;Invalid token type&quot;, nameof(other));
    }

    public override bool Equals(StreamSequenceToken other)
    {
        var token = other as RedisStreamSequenceToken;
        return token != null &amp;&amp; SequenceNumber == token.SequenceNumber &amp;&amp; EventIndex == token.EventIndex;
    }
}
</pre></div>


<p>Finally, I implemented the <strong>IBatchContainer</strong> interface. This interface enables you to define a container for an event or a list of events. However, in my implementation, I made it to only encapsulate a single event or message from Redis streams. </p>



<p>Orleans streams support having heterogeneous types. This means that the queueing system can hold different types of data structures and Orleans will be able to deserialize it with your custom deserializer into the proper type required by the the event consumers.</p>



<p>It also has a generic method called <strong>GetEvents&lt;T&gt;()</strong>. This method enables the pulling agent to check if the batch container has the type of data required by the subscribed grain. </p>



<p>Below is my implementation:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RedisStreamBatchContainer : IBatchContainer
{
    public StreamId StreamId { get; }

    public StreamSequenceToken SequenceToken { get; }
    public StreamEntry StreamEntry { get; }
    public RedisStreamBatchContainer(StreamEntry streamEntry)
    {
        StreamEntry = streamEntry;
        var streamNamespace = StreamEntry.Values&#91;0].Value;
        var steamKey = StreamEntry.Values&#91;1].Value;
        StreamId = StreamId.Create(streamNamespace!, steamKey!);
        SequenceToken = new RedisStreamSequenceToken(StreamEntry.Id);
    }
    public IEnumerable&lt;Tuple&lt;T, StreamSequenceToken&gt;&gt; GetEvents&lt;T&gt;()
    {
        List&lt;Tuple&lt;T, StreamSequenceToken&gt;&gt; events = new();
        var eventType = typeof(T).Name;
        if (eventType == StreamEntry.Values&#91;2].Value)
        {
            var data = StreamEntry.Values&#91;3].Value;
            var @event = JsonSerializer.Deserialize&lt;T&gt;(data!);
            events.Add(new(@event!, SequenceToken));
        }
        return events;
    }

    public bool ImportRequestContext()
    {
        return false;
    }
}
</pre></div>


<p>One last thing you should note is that when setting up the Factory settings ensure the same configuration of queues is set on both the Orleans server and the client.</p>



<p>For instance, if I want 4 queues to be created then I have to set the number of queues in both the client and the server to be the same. The code below shows that:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
// client
using IHost host = new HostBuilder()
     .UseOrleansClient(clientBuilder =&gt;
     {
         clientBuilder.Services.AddSingleton&lt;IDatabase&gt;(sp =&gt;
         {
             IDatabase db = ConnectionMultiplexer.Connect(&quot;localhost&quot;).GetDatabase();
             return db;
         });
         clientBuilder.UseLocalhostClustering();

         clientBuilder.AddPersistentStreams(&quot;RedisStream&quot;, RedisStreamFactory.Create, null);
         clientBuilder.ConfigureServices(services =&gt;
         {
             services.AddOptions&lt;HashRingStreamQueueMapperOptions&gt;(&quot;RedisStream&quot;)
                 .Configure(options =&gt;
                 {
                     options.TotalQueueCount = 4;
                 });

         });
     })
     .ConfigureLogging(logging =&gt; logging.AddConsole())
     .Build();

// server
var builder = new HostBuilder()
    .UseOrleans(silo =&gt;
    {
        silo.UseLocalhostClustering();
        silo.Services.AddSingleton&lt;IDatabase&gt;(sp =&gt;
        {
            return ConnectionMultiplexer.Connect(&quot;localhost&quot;).GetDatabase();
        });
        silo.ConfigureLogging(logging =&gt; logging.AddConsole());
        silo.AddMemoryGrainStorage(&quot;PubSubStore&quot;);
        silo.AddPersistentStreams(&quot;RedisStream&quot;, Provider.RedisStreamFactory.Create, null);
        silo.AddMemoryGrainStorageAsDefault();
    }).UseConsoleLifetime();
builder.ConfigureServices(services =&gt;
{
    services.AddOptions&lt;HashRingStreamQueueMapperOptions&gt;(&quot;RedisStream&quot;)
        .Configure(options =&gt;
        {
            options.TotalQueueCount = 4;
        });
    services.AddOptions&lt;SimpleQueueCacheOptions&gt;(&quot;RedisStream&quot;);
});
using IHost host = builder.Build();
</pre></div>


<p>Thanks for reading through, a working project that stitches everything together can be seen <a href="https://github.com/sammychinedu2ky/RedisStreamsInOrleans" target="_blank" rel="noreferrer noopener">here</a>.</p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/custom-redis-streams-provider-for-orleans/">Custom Redis Streams Provider for Orleans</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://swacblooms.com/custom-redis-streams-provider-for-orleans/feed/</wfw:commentRss>
			<slash:comments>13</slash:comments>
		
		
			</item>
		<item>
		<title>Creating a Custom File Storage Provider for Microsoft Orleans</title>
		<link>https://swacblooms.com/creating-a-custom-file-storage-provider-for-microsoft-orleans/?utm_source=rss&amp;utm_medium=rss&amp;utm_campaign=creating-a-custom-file-storage-provider-for-microsoft-orleans</link>
					<comments>https://swacblooms.com/creating-a-custom-file-storage-provider-for-microsoft-orleans/#comments</comments>
		
		<dc:creator><![CDATA[Samson Amaugo]]></dc:creator>
		<pubDate>Fri, 07 Jun 2024 08:47:50 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://swacblooms.com/?p=1712</guid>

					<description><![CDATA[<p>Hello &#x1f44b;, so in today&#8217;s article I will be writing about how I created a custom file Storage provider for Microsoft Orleans. The purpose of this project is not to create a production ready storage provider but something I can use to easily visualize the data persistence during app development and also get a better understanding of persistence in Orleans &#x1f605;. The image below illustrates what the custom file persistence layer looks like To create a custom storage provider, I had to implement the IGrainStorage interface which is structured below: Looking at the interface you can see that they all </p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/creating-a-custom-file-storage-provider-for-microsoft-orleans/">Creating a Custom File Storage Provider for Microsoft Orleans</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hello <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" />, so in today&#8217;s article I will be writing about how I created a custom file Storage provider for Microsoft Orleans.</p>



<p>The purpose of this project is not to create a production ready storage provider but something I can use to easily visualize the data persistence during app development and also get a better understanding of persistence in Orleans <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>



<p>The image below illustrates what the custom file persistence layer looks like</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="560" height="552" src="https://swacblooms.com/wp-content/uploads/2024/06/image.png" alt="Microsoft Orlans Custom File Storage Provider" class="wp-image-1713" srcset="https://swacblooms.com/wp-content/uploads/2024/06/image.png 560w, https://swacblooms.com/wp-content/uploads/2024/06/image-300x296.png 300w, https://swacblooms.com/wp-content/uploads/2024/06/image-65x65.png 65w" sizes="(max-width: 560px) 100vw, 560px" /></figure></div>


<p>To create a custom storage provider, I had to implement the <strong>IGrainStorage</strong> interface which is structured below:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public interface IGrainStorage
{
  Task ReadStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState);
  Task WriteStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState);
  Task ClearStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState);
}
</pre></div>


<p>Looking at the interface you can see that they all have similar signature, bearing &#8220;<strong>stateName</strong>&#8220;, &#8220;<strong>grainId</strong>&#8221; and &#8220;<strong>grainState</strong>&#8221; as parameters.</p>



<p>A Grain can have multiple states stores so the &#8220;stateName&#8221; helps to identity the particular state during data persistence from a Grain.</p>



<p>A Grain&#8217;s Id comprises of two important things: The type and the key.</p>



<p>Let&#8217;s say I have a Grain called &#8220;VotingGrain&#8221;.  By default the Grain&#8217;s type is identified as &#8220;<strong>voting</strong>&#8220;</p>



<p>The key is the unique value sent by the client to interact with the a particular Grain. In the code below the Grain&#8217;s key is idenitified as &#8220;<strong>football</strong>&#8220;.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
var soccerPolls = client.GetGrain&lt;IVotingGrain&gt;(&quot;football&quot;);
</pre></div>


<p>And finally from the <strong>IGrainStorage </strong>interface, the <strong>IGrainState&lt;T&gt;</strong> parameter holds a reference to the data in a Grain.</p>



<p>To use the <strong>Options</strong> pattern while creating the File Storage provider extension, I created the the type below:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
    public class FileConfigurationOptions
    {
        // the location of the folder to use as a store
        public string? StorePath { get; set; }

        // the section in the config file to retrieve store path from
        public const string MyFileConfiguration = &quot;FileConfiguration&quot;;
    }
</pre></div>


<p>Next, I created the file storage provider by implementing the <strong>IGrainStorage</strong> interface</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class FileStorage : IGrainStorage
{
    private void _ensureFileExists(string filePath)
    {
        if (!File.Exists(filePath))
        {
            var directory = Path.GetDirectoryName(filePath);
            Directory.CreateDirectory(directory!);
            File.Create(filePath).Close();
        }
    }

    private string _getFilePath(GrainId grainId, string stateName)
    {
        return Path.Combine(_options.Value.StorePath!, grainId.Type.ToString()!, grainId.Key.ToString()!, stateName + &quot;.json&quot;);
    }
    private readonly IOptions&lt;FileConfigurationOptions&gt; _options;
    public FileStorage(IOptions&lt;FileConfigurationOptions&gt; options) =&gt; _options = options;
    public Task ClearStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState)
    {
        var path = _getFilePath(grainId, stateName);
        File.Delete(path);
        return Task.CompletedTask;
    }

    public async Task ReadStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState)
    {
        var path = _getFilePath(grainId, stateName);
        _ensureFileExists(path);
        var data = await File.ReadAllTextAsync(path);
        if (string.IsNullOrEmpty(data)) return;
        grainState.State = JsonSerializer.Deserialize&lt;T&gt;(data)!;
    }

    public async Task WriteStateAsync&lt;T&gt;(string stateName, GrainId grainId, IGrainState&lt;T&gt; grainState)
    {
        var path = _getFilePath(grainId, stateName);
        _ensureFileExists(path);
        var data = JsonSerializer.Serialize(grainState.State);
        await File.WriteAllTextAsync(path, data);
    }
}
</pre></div>


<p>The storage implementation above uses a combination of Grain&#8217;s ID Type, Grain&#8217;s ID <strong>Key </strong>and Grain&#8217;s <strong>stateName </strong>to create the file path to the file that holds the stored data.</p>



<p>One limitation of the storage provider mentioned above is that I didn&#8217;t use ETags to prevent concurrency issues. If the <strong>AlwaysInterleave </strong>attribute setting is enabled for a Grain&#8217;s method that writes to storage, it could cause errors due to race conditions. However, without this configuration, everything works fine <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>Next, I created my extension method to easily help me setup the File Storage provider in Orleans. I created two methods, the first overload allows you to set the path of the &#8220;<strong>filestore</strong>&#8221; directly in the code while the second one reads the path from the Orleans configuration file.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
    public static class FileStorageExtensions
    {
        public static IServiceCollection AddFileStorage(this IServiceCollection services, Action&lt;FileConfigurationOptions&gt; configureOptions)
        {
            services.AddOptions&lt;FileConfigurationOptions&gt;()
                       .Configure(configureOptions);
            return services.AddKeyedSingleton&lt;IGrainStorage, FileStorage&gt;(&quot;fileStateStore&quot;);
        }

        public static IServiceCollection AddFileStorage(this IServiceCollection services)
        {
            services.AddOptions&lt;FileConfigurationOptions&gt;()
               .Configure&lt;IConfiguration&gt;((settings, configuration) =&gt;
               {
                   configuration.GetSection(FileConfigurationOptions.MyFileConfiguration).Bind(settings);
               });
            return services.AddKeyedSingleton&lt;IGrainStorage, FileStorage&gt;(&quot;fileStateStore&quot;);
        }
    }
</pre></div>


<p>You can see above that Orleans makes use of the <strong>AddKeyedSingleton </strong>feature to identify the store implementation.</p>



<p>And finally, I configured Orleans to use the custom file provider below:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
IHostBuilder builder = new HostBuilder()
    .UseOrleans(silo =&gt;
    {
        silo.UseLocalhostClustering();
        silo.Services.AddFileStorage();
    });

builder.ConfigureAppConfiguration((context, config) =&gt;
{
    config.AddJsonFile(&quot;path_to_config_file.json&quot;, optional: true);
});
using IHost host = builder.Build();

await host.RunAsync();
</pre></div>


<p>Now when I select the store using the &#8220;<strong>fileStateStore</strong>&#8221; value in the <strong>PersistentState </strong>attribute of the Grain&#8217;s constructor, the Grain persists its data using the custom store which can be seen in the code below:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class PersonGrain : IGrainBase, IPersonGrain
{
    IPersistentState&lt;PersonState&gt; state;
    public IGrainContext GrainContext { get; }
    public PersonGrain(IGrainContext context, &#91;PersistentState(&quot;personState&quot;, &quot;fileStateStore&quot;)] IPersistentState&lt;PersonState&gt; state) =&gt; (GrainContext, this.state) = (context, state);

    public async Task AddName(string name)
    {
        var context = this.GrainContext;
        state.State.Name = name;
        await state.WriteStateAsync();
    }
    public Task&lt;string&gt; GetName()
    {
        return Task.FromResult($&quot;My name is {state.State.Name}&quot;);
    }
}


public class PersonState
{
    public string? Name { get; set; }
}
</pre></div>


<p>Thanks for reading through, the link to the project can be seen <a href="https://github.com/sammychinedu2ky/Microosft-Orleans-CustomFileStorageProvider-/tree/master">here</a>.</p>



<p>To learn more about Grain persistence in Microsoft Orleans click <a href="https://learn.microsoft.com/en-us/dotnet/orleans/grains/grain-persistence/?WT.mc_id=DT-MVP-5004830">here</a>.</p>



<p>Bye <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>The post <a rel="nofollow" href="https://swacblooms.com/creating-a-custom-file-storage-provider-for-microsoft-orleans/">Creating a Custom File Storage Provider for Microsoft Orleans</a> appeared first on <a rel="nofollow" href="https://swacblooms.com">Swacblooms&#x1f98b;</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://swacblooms.com/creating-a-custom-file-storage-provider-for-microsoft-orleans/feed/</wfw:commentRss>
			<slash:comments>95</slash:comments>
		
		
			</item>
	</channel>
</rss>