Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
152118 stories
·
33 followers

Critter Stack and JasperFx Retrospective for 2025

1 Share

At least professionally, I tend to be mostly focused on what’s next on the road map or upcoming client work or long planned strategic improvements to the Critter Stack (Marten and Wolverine). One of the things I do every year is to write out a blog post stating the technical goals for the OSS projects that I lead, with this year’s version, Critter Stack Roadmap for 2026 already up (but I’m already going to publish a new version later this week). I’ll frequently look back to what I wrote in the previous January and be frustrated by the hoped for initiatives that still haven’t been completed or even started. All the same though, 2025 was a very productive year for the Critter Stack and there’s plenty of accomplishments and improvements worth reflecting on.

JasperFx Software in 2025

JasperFx Software more than doubled our roster of ongoing support clients while doing quite a bit of one off consulting and delivery work as well. The biggest improvement and growth is that I’ve stopped fretting on a daily basis about whether I gambled my family’s financial well being on an ego driven attempt to stave off a mid life crisis and started confidently planning the company’s future around what appears to be a very successful and promising technical toolset.

Along the way, we helped our clients through interactions on Discord, Slack, MS Teams (I’m not yet a fan), Zoom, and GitHub. Common topics for Critter Stack usage included:

  • Designing long lived workflows
  • Event Sourcing usage
  • Resiliency strategies of all sorts
  • Multi-Tenancy
  • Dealing with Concurrency. Lots of concurrency related issues
  • Test Automation
  • Quite a bit of troubleshooting
  • Instrumentation

If you would like any level of help with your Critter Stack usage, feel free to reach out to sales@jasperfx.net for a conversation about what or how JasperFx could help you out.

Marten in 2025

2024 was an insanely busy year for Marten improvements, and after that, I feel like there just wasn’t much lacking anymore in Marten’s feature set for productive event sourcing. You can definitely see Marten development slowed down a bit in 2025. Even so, we had 16 folks commit code this past year — with far more folks contributing through Discord discussions and feedback in GitHub issues.

Some of the highlights were:

  • Marten turned 10 years old!
  • This has been a continuously rocky voyage, but we greatly improved Marten’s support for testing asynchronous processing with new helpers. A lot of folks contributed to that over 2025.
  • For a JasperFx Software client, we enabled Inline “Side Effect” processing
  • Marten 8.0 dropped at the first of June, which included:
    • A big consolidation and reorganization of the shared dependencies underneath Marten and Wolverine
    • A lot of improvements to Marten’s Projections API including much better (we hope) options for writing explicit code and a streamlined API for “event slicing and grouping” in multi-stream projections
    • JasperFx Software built the Stream Compacting feature for our largest client as a mechanism to keep a busy system running smoothly over time by keeping the database size relatively stable
    • Anne wrote a new tutorial for using Marten as an event store in systems
  • We added a lot more support for strong typed identifiers in different usage scenarios to Marten throughout the year. I won’t claim there isn’t still some potential problems out there, but dammit, we tried really hard on that front
  • JasperFx Software added the ability to alter event metadata on an event by event basis for one of our clients. That same 8.4 release added user name tracking to event metadata
  • Again in collaboration with JasperFx clients, we added far more metrics publishing to understand the Async Daemon behavior in production
  • Switched Marten from the venerable TPL DataFlow library to using System.Threading.Channels. I was very happy with how smoothly that went after the first day of toe stubbing.
  • Event enrichment hook in aggregation projections for more efficient processing. That was a long missing feature — but this is getting some additional improvements in the next couple weeks

After saying that I felt like Marten was essentially “done” at the beginning of this section, I think we actually do have a pretty ambitious set of enhancements for Marten projection support and cross-document querying teed up for early 2026.

All the same though, I’ll stand by my recent statements that Marten is clearly the best technical tool for Event Sourcing on the .NET platform and competitive with anything else out there in other ecosystems.

Wolverine in 2025

Buckle in, this list is much longer and I’m only going to hit the highlights because Wolverine development was crazily busy in 2025:

  • Wolverine had 66 different people contribute code in 2025. Some of those are from little pull requests to improve documentation (i.e., fixing my errors in markdown files), but that number dramatically undercounts the contribution from people in GitHub issues and Discord discussions. And also, those little pull requests to improve documentation are very much appreciated by me and I think they definitely improve the project in the whole.
  • Lots of improvements to Wolverine’s support for Modular Monolith architectures
  • Yet more support for strong typed identifiers. Curse those little maggots, but some of you really, really like them
  • Wolverine 3.6 was a huge release that included the declarative persistence helpers that leads to much simpler application code
  • Wolverine 3.13 improved Wolverine.HTTP with [AsParameters] binding support
  • Wolverine 4.0 brought the consolidation of shared dependencies with Marten as well as Multi-Tenancy through separate databases with Entity Framework Core (for a JasperFx client).
  • More Wolverine transport options support the “named broker” approach such that you can connect to multiple Rabbit MQ brokers, Azure Service Bus namespaces, Kafka brokers, or Amazon SQS endpoints from one system
  • Wolverine got much better support for “hybrid” HTTP endpoint/handler combinations (in collaboration with a JasperFx support client)
  • JasperFx working with another client improved the usability of F# idioms with Wolverine
  • Quite a few improvements to the tracked session test automation helpers in Wolverine as a result of multiple JasperFx client engagements
  • Wolverine 5.0 was huge, with the Partitioned Sequential Messaging being my pick as the most important new feature
  • New messaging transport options including GCP Pubsub, SignalR, Redis, an HTTP options, and a NATS.io option ready to go in the first release of 2026. Dang.

And there was a massive rush of activity at the end of the year as I scurried to address issues and requests from recent JasperFx clients for yet more resiliency, error handling, instrumentation, and inevitably some bugs. I’ll be writing a blog post later this week to go over the new additions from the US Thanksgiving holiday through the end of 2025.

I guess the big takeaway is that Wolverine improved a lot in 2025 and I expect that trend to continue at least during the early part of 2026. I would argue that regardless of exactly how Wolverine stacks up on features and usability compared to other options in .NET that Wolverine is improving and innovating much faster than any of its competitors.

Me in 2025

I’m maybe working at a bit of an unsustainable pace for the longer term, but I think I’m good for at least one more year at this pace. At the end of the day though, I feel extremely fortunate to be living out my long term professional dream to have my own company centered around the OSS tools that I’ve created or led.



Read the whole story
alvinashcraft
42 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Podcast: Writing is telepathy: AI tools, automation, an intentionally offline life -- conversation with CT Smith

1 Share
In this episode, Fabrizio (passo.uno) and I talk with CT Smith, who writes on a blog at docsgoblin.com and works as a documentation lead for Payabli. Our conversation covers how CT uses AI tools like Claude in her documentation workflow, why she builds tooling that doesn't depend on AI, her many doc-related projects and experiments, and how she balances a tech writing career with an intentionally offline life in rural Tennessee. We also get into reading habits, the fear of skill atrophy from AI reliance, and where the tech writer role might be headed.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Ikea’s new $4 USB-C charger is cheaper than Apple’s and Anker’s

1 Share
Ikea’s 20W Sjöss charger plugged into a wall outlet with a USB-C cable attached.
It’s only available in one color option. | Image: Ikea

Ikea has released the most affordable version of its Sjöss USB-C chargers yet. Following an $8 30W version that launched in 2024 and a $35 65W option that debuted earlier this year, the new 20W Sjöss is now available online and through many of Ikea's physical US stores for just $3.99.

That's considerably cheaper than Apple's $19 20W USB-C Power Adapter while being comparable in size, and cheaper than Anker's $11.99 Nano Pro 20W USB-C charger. However, Ikea's new $4 charger is much larger than Anker's, and is about 26 grams heavier. If you're a traveler trying to pack as light as possible, Anker's may be the better choice. If you're a travel …

Read the full story at The Verge.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Python's else on Loops: The Feature You're Not Using

1 Share

Python has a feature that surprises developers coming from other languages: you can attach an else clause to while and for loops.

This isn't a bug or obscure syntax—it's an intentional design choice, and it solves a real problem.

The Rule

The else block runs only if the loop completes without encountering a break.

while condition:
    if found_target:
        break
    # keep searching
else:
    # This runs only if we never broke out
    print("Search completed without finding target")

Why This Matters

Consider a search loop. Without else, you need a flag variable:

found = False
index = 0
while index < len(numbers):
    if numbers[index] == target:
        found = True
        break
    index += 1

if not found:
    print("Not found")

That's three extra operations:

  1. Initialize found = False
  2. Set found = True when found
  3. Check if not found after the loop

With else:

index = 0
while index < len(numbers):
    if numbers[index] == target:
        print(f"Found at index {index}")
        break
    index += 1
else:
    print("Not found")

No flag. The else replaces the conditional check entirely.

The Mental Model

Think of while...else as "while...nobreak":

  • If break executes → else is skipped
  • If the loop exits normally (condition becomes False) → else runs

Real-World Use Case: Retry Pattern

This pattern is common when connecting to APIs, databases, or external services:

max_attempts = 3
attempt = 0

while attempt < max_attempts:
    attempt += 1
    print(f"Attempt {attempt}...")

    if try_connection():
        print("Connected!")
        break
else:
    print("All attempts failed. Service unavailable.")

The else fires only when all retries are exhausted—exactly when you need to handle failure.

Note: This is a simplification. Production retry logic typically includes exponential backoff and jitter to avoid overwhelming the server with simultaneous retries from multiple clients.

Why It's Underused

The naming is confusing. else after a loop doesn't sound like "run if no break." It sounds like it should be connected to a conditional.

Some Python developers have argued it should have been called nobreak or finally (though finally already means something different in Python's exception handling).

The syntax won't change, so the best approach is to internalize the mental model: else means nobreak.

When To Use It

  • Search loops: Handle "not found" without a flag
  • Retry patterns: Handle "all attempts exhausted"
  • Validation loops: Handle "no valid option found"

Important: It's only useful for loops that use break for early exit. If your loop doesn't have a break, the else will always run—which is rarely what you want.

Bonus: Common Infinite Loop Bugs

Since we're talking about while loops, here are three bugs that waste debugging time:

Bug 1: Forgetting to update the variable

count = 0
while count < 5:
    print(count)
    # Forgot: count += 1

Bug 2: Wrong update direction

count = 10
while count > 0:
    print(count)
    count += 1  # Should be: count -= 1

Bug 3: Off-by-one with not-equals

x = 3
while x != 10:
    x += 2  # x goes 3, 5, 7, 9, 11... never equals 10

Fix: Use x < 10 instead of x != 10.

Debugging tip: Add a safety counter during development:

safety = 0
while condition and safety < 1000:
    safety += 1
    # your code

if safety >= 1000:
    print("WARNING: Loop hit safety limit!")

From my upcoming book "Zero to AI Engineer: Python Foundations."

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Playwright vs. Selenium: A 2026 Architecture Review

1 Share

1. Introduction: Beyond the API Surface

By 2026, the debate between Playwright and Selenium has largely moved beyond "syntax preference" or "language support." For the senior automation architect, the choice is no longer about whether you prefer driver.find_element or page.locator; it is a fundamental decision about infrastructure topology and protocol efficiency.

Historically, browser automation was viewed as "scripting": sending a command to click a button and waiting for a result. Today, it is a critical layer of distributed infrastructure. We run tests in ephemeral containers, scrape data behind aggressive WAFs, and automate complex authentications protected by hardware-bound credentials. In this high-stakes environment, the underlying architecture of your automation tool dictates its reliability, speed, and maintainability.

This review dismantles the internal mechanics of Selenium (including the W3C WebDriver BiDi evolution) and Playwright. We will analyze why architectural decisions made in 2004 still constrain Selenium today, and how Playwright’s "headless-first" event loop aligns with the reality of the modern web.

2. The Protocol Gap: HTTP vs. WebSockets

The single most defining difference between the two frameworks is the communication protocol used to drive the browser. This is not an implementation detail; it is the root cause of nearly every performance and stability difference between the two.

Selenium: The HTTP Request/Response Cycle

Selenium’s architecture is built on the WebDriver W3C Standard. At its core, it is a RESTful HTTP protocol. Every single action in a Selenium script triggers a discrete HTTP request:

  1. Client: Sends POST /session/{id}/element (Find element)
  2. Driver: Receives request, translates to browser internal command, waits for browser, returns JSON response.
  3. Client: Parses JSON.
  4. Client: Sends POST /session/{id}/element/{id}/click (Click element)
  5. Driver: Receives request, executes click, returns JSON.

This "chatty" architecture introduces Control Loop Latency. Between every command, there is network overhead, serialization, and deserialization. In a local environment, this is negligible (milliseconds). In a distributed grid (e.g., running tests on Sauce Labs or BrowserStack from a CI runner), these round-trip times accumulate, creating a "stop-and-go" execution rhythm that is inherently slower and prone to race conditions.

Playwright: The Persistent WebSocket

Playwright abandons standard HTTP for a single, persistent WebSocket connection (leveraging the Chrome DevTools Protocol or CDP). Once the connection is established, the channel remains open.

  • Bi-Directional: The browser can push events to the script (e.g., "network request failed" or "DOM node added") without the script asking for them.
  • Command Batching: Playwright can send multiple instructions down the pipe without waiting for HTTP handshakes.
  • Low Overhead: Binary data (like screenshots) streams efficiently without the massive Base64 overhead typical of JSON payloads.

A comparison diagram titled

3. Execution Models & The "Auto-Wait" Myth

Senior engineers often cite Playwright’s "Auto-Wait" as a key feature. However, understanding how it works architecturally explains why Selenium struggles to replicate it, even with "Explicit Waits."

Selenium: External Polling

When you use WebDriverWait in Selenium, the logic lives in your client script (Python/Java/C#). The script effectively spams the browser driver with HTTP requests:
"Is it visible? No. (Wait 500ms). Is it visible? No. (Wait 500ms). Is it visible? Yes."
This polling happens outside the browser process. It creates network noise and, crucially, a race condition window. The element might flicker into visibility and back out between poll intervals, causing the test to fail or interact with a detaching element.

Playwright: Internal Event Listeners

Playwright compiles your locator logic and injects it directly into the browser context via the CDP session. The "waiting" happens inside the browser's own event loop.
Playwright hooks into requestAnimationFrame and the browser’s painting cycle. It checks for element stability (is the bounding box moving?) and actionability (is it covered by a z-index overlay?) in the same render loop as the application itself. The command to "click" is only executed when the browser itself confirms the element is ready. This atomic "Check-and-Act" mechanism eliminates the race conditions inherent to external polling.

4. Network Interception: Proxy vs. Native

In 2026, automation is rarely just about clicking buttons. It requires mocking API responses, injecting headers, and blocking analytics trackers.

Selenium historically required a "Man-in-the-Middle" (MITM) proxy (like BrowserMob) to intercept network traffic. This added a massive point of failure: certificate trust issues, decreased throughput, and complex infrastructure setup. While Selenium 4+ introduced NetworkInterceptor, it is a patchwork implementation on top of the WebDriver protocol, often limited in granularity and prone to compatibility issues across different browser drivers.

Playwright gains network control for free via its architecture. Because it communicates via CDP (or the Firefox/WebKit equivalents), it sits between the browser’s network stack and the rendering engine. It can pause, modify, or abort requests natively without a proxy server. This allows for:

  • Zero-latency mocking: The request never leaves the browser process.
  • Reliability: No SSL certificate installation required on the host machine.
  • Granularity: Routing logic (glob patterns, regex) is evaluated instantaneously.

5. Ecosystem & The "Selenium 5" Promise

As of 2026, Selenium 5 has fully embraced WebDriver BiDi, a standardized effort to bring bi-directional communication (WebSockets) to the WebDriver standard. This is Selenium’s answer to Playwright.

The Reality of BiDi:
While BiDi allows Selenium to receive events (like console logs) without polling, it is fundamentally an "add-on" to a legacy architecture. The vast ecosystem of Selenium Grids, cloud providers, and existing test suites relies on the HTTP Request/Response model. Migrating a massive Selenium codebase to utilize BiDi features often requires significant refactoring, bringing the effort parity close to a full migration to Playwright.

Playwright’s Advantage:
Playwright was designed after the Single Page Application (SPA) revolution. Its "Browser Context" model—which allows spinning up hundreds of isolated, incognito-like profiles within a single browser process—is an architectural leap over Selenium’s "One Driver = One Browser" resource-heavy model. This makes Playwright exponentially cheaper to run at scale in containerized environments (Kubernetes/Docker).

6. Decision Matrix: Choosing in 2026

When should you stick with the incumbent, and when should you adopt the challenger?

Feature Selenium (w/ BiDi) Playwright
Primary Protocol HTTP (Restful) WebSocket (Event-driven)
Wait Mechanism External Polling Internal Event Loop (RAF)
Language Support Java, C#, Python, Ruby, JS TS/JS, Python, Java,.NET
Legacy Browsers Excellent (IE Mode support) Non-existent (Modern engines only)
Mobile Support Appium (Native Apps) Experimental / Web only
Scale Cost High (1 Process per Test) Low (Contexts per Process)

Stick with Selenium if:

  • You require testing on legacy browsers (IE11) or specific older versions of Chrome/Firefox.
  • You are integrated heavily with Appium for native mobile testing.
  • Your team is strictly Java/C# and prefers a synchronous, blocking API style.

Migrate to Playwright if:

  • You are testing modern SPAs (React, Vue, Angular) where component re-rendering causes flakiness in Selenium.
  • You need high-performance scraping or data extraction (Network interception is critical).
  • You want to reduce CI/CD infrastructure costs by 30-50% via browser contexts.

7. Conclusion

In 2026, Playwright is not just a "better Selenium"—it is a different species of tool. By coupling tightly with the browser internals via WebSockets, it removes the layers of abstraction that caused a decade of "flaky tests."

Selenium remains a titan of interoperability and standard compliance. Its W3C WebDriver standard ensures it will run on anything, forever. But for the engineering team tasked with building a reliable, high-speed automation pipeline for a modern web application, Playwright’s architecture offers the path of least resistance. It solves the hard problems of synchronization and latency at the protocol level, allowing you to focus on the test logic, not the sleep statements.

Read the whole story
alvinashcraft
3 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Why Every Developer Should Embrace “Reading Code” as Much as Writing It

1 Share

When we talk about improving as a developer, most advice focuses on writing code: learning new frameworks, mastering algorithms, or optimizing performance. But one of the most underrated skills in programming isn’t writing—it’s reading code.

The Hidden Superpower

Think about it. Every day, developers spend hours working with code they didn’t write: legacy systems, open-source libraries, or teammate contributions. Being able to quickly understand someone else’s code is like having a superpower. It helps you debug faster, avoid introducing bugs, and even learn new patterns you hadn’t considered.

Yet, many developers skip this practice. We’re wired to solve problems by coding, not by reading. But reading code can teach you how others think, reveal idiomatic uses of a language, and expose you to clever techniques that you can later apply in your own projects.

Start Small, Read Daily

You don’t need to dive into huge codebases right away. Start with something small:

  1. Open-source projects on GitHub.
  2. Code snippets on StackOverflow.
  3. Even a teammate’s pull request.

As you read, ask questions: Why did they structure it this way? Could it be simpler? How does this function interact with the rest of the system?

This approach trains you to think like a developer before you type a single line of code.

The Debugging Bonus

Reading code is also the secret to better debugging. Often, the bug isn’t where you think it is. By systematically reading through the code, you can understand the flow, spot edge cases, and find issues before they turn into hours of frustration.

It’s like being a detective: the more you read, the more clues you pick up, and the faster you solve the mystery.

Learning Beyond Tutorials

Tutorials and courses are great for learning syntax, but real growth comes from reading real-world code. You’ll see patterns, anti-patterns, trade-offs, and compromises that tutorials never teach. Over time, your own code starts to look cleaner, more maintainable, and more efficient because you’ve absorbed best practices by osmosis.

A Habit Worth Building

Try setting aside 20–30 minutes a day to read someone else’s code. Treat it like reading a book: analyze, reflect, and learn. Pair it with your coding time, and you’ll notice subtle improvements in your speed, design sense, and problem-solving skills.

Read the whole story
alvinashcraft
3 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories