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

Your agent’s guardrails have a bypass

1 Share

The deployments are real now: agents with tools, credentials, and the autonomy to act on an organization’s behalf. Every agent carries the same requirement: policies must be enforced, approvals must actually gate actions, and there must be evidence of what happened. And every framework answers that requirement differently, with mechanisms that were designed for observability, not governance. As agents grow more capable, their execution paths multiply: subagents, retries, batch entry points, background tasks. The governance question moves from “did we write a guardrail?” to “is it enforced on every path, and can we prove it?” 

Here is an example of this gap in practice. 

A team builds a customer support agent. It can look up accounts, draft replies, and issue refunds. Compliance sets two rules: refunds above a threshold need human approval, and account data never reaches the reply channel unredacted. The team does what their framework documentation suggests, adding a guardrail callback on tool calls and tool outputs. 

While performing quarter-end financial closing, it was discovered that there is a large gap exceeding the discretionary customer refund budget. The system has been issuing refunds erroneously. The incident review finds three things. The approval guard threw an exception on a malformed refund request; the framework dispatcher caught the error, logged a warning, and executed the refund anyway, which is its documented default. The output scanner never saw one egress path, because a batch entry point emitted no callback at all: the guard was attached to the interactive path, and the batch path simply never fired it. And nobody could produce evidence of what either guard actually evaluated, because callbacks observed values without recording anything that binds them to what executed. 

Nothing here is exotic. The team followed the instructions. The instructions were the problem.

The failure class: enforcement attached to one path, while the runtime grows paths. The fix is structural, not more callbacks.
The failure class: enforcement attached to one path, while the runtime grows paths. The fix is structural, not more callbacks.

The hooks you have are not a governance surface

We catalogued the interception surfaces of the mainstream agent frameworks from their primary documentation and source. LangChain’s BaseCallbackHandler defines 20 lifecycle events, and the dispatcher discards handler return values, so a callback cannot block or rewrite anything; a handler exception is caught and swallowed unless the author opts into raise_error, which defaults to false. CrewAI’s event bus registers 78 typed event kinds, all observe-only. LlamaIndex’s instrumentation module is telemetry by design: no return value, exception, or mutation reaches the underlying action. The OpenAI Agents SDK exposes lifecycle hooks that observe and guardrails that can block, but its input guardrails race the first model call unless you set a flag. Semantic Kernel’s filters genuinely block; but when registered through dependency injection, execution order is documented as not guaranteed and ordering decides whether redaction runs before egress. 

Count the lifecycle surfaces alone: LangChain exposes 20 callback events, CrewAI 78, the OpenAI Agents SDK seven, and Semantic Kernel three, while LlamaIndex ships two coexisting observability surfaces. Payloads range from untyped dictionaries to typed contexts. Control semantics range from none to full block-and-modify. Failure behavior ranges from silently swallowed to propagated. And not one of these frameworks ships a conformance suite that a controls author can run to verify that a deny stops the action. Every guarantee your control depends on is framework-specific folklore.

Why builders should care

If you run agents in production, you inherit two problems: controls behave differently across frameworks, and you cannot reliably prove that they ran. 

Control builders must create and maintain a separate adapter for each framework. Worse, each framework answers the most important question differently: when a control denies an action, does the action always stop? 

Framework builders face the other side of the same problem. Enterprise customers need approvals, policy checks, audit records, and data controls, so each framework must build and maintain these features itself. A shared contract changes the practical outcomes for all three: write controls once and reuse them across frameworks, verify enforcement instead of assuming it, get audit evidence by construction, and stop paying the per-framework integration tax on every governance requirement.

Agent Hooks: One governance contract for the ecosystem, testable on both sides

Today we’re publishing Agent Hooks, specified as AGENT-HOOKS-0.1: an open, framework-neutral governance contract for AI agents, and a common interoperability layer that any framework can implement and any control can target. It ships with SDKs in Python, TypeScript, .NET, Rust, and Go, a 47-scenario conformance kit that makes “supported” a testable claim, and a first-class implementation merged into Microsoft Agent Framework’s core. The contract is deliberately small: eight interception points that bracket the agent loop, one context payload, three verdicts, and normative obligations on the host. Controls integrate against it once. Frameworks implement it once. The M×N adapter matrix becomes M+N.

Twenty bespoke adapters, or one contract with a conformance kit on each side of it.
Twenty bespoke adapters, or one contract with a conformance kit on each side of it.

What it looks like

An interceptor is a few lines. This one enforces the refund rule from the opening incident:

from agent_hooks import Interceptor, Verdict class RefundGuard(Interceptor): def intercept(self, context) -> Verdict: if context["interception_point"] != "pre_tool_call": return Verdict.allow() call = context["tool_call"] if call["name"] == "issue_refund" and call["args"]["amount"] > 500: return Verdict.escalate(reason="refund_over_limit", message="requires human approval") return Verdict.allow()

And installing the full contract in Microsoft Agent Framework is one factory call:

pip install agent-framework-core[agent-hooks] agent = Agent(client=client, tools=[issue_refund], middleware=[create_agent_hooks_middleware([RefundGuard()])])

That single call installs enforcement at every point of the loop: it is deliberately impossible to install part of the contract and believe you have all of it.

How it works: Emitting and enforcing

The eight points bracket the loop: agent_startup, input, pre_model_call, post_model_call, pre_tool_call, post_tool_call, output, agent_shutdown. At each, the host builds an AgentContext, a tiered JSON payload with a small required core (agent, session, sequence, timestamp, and the target under evaluation), per-point required fields, and namespaced extensions. The target is the one value a transform may rewrite, pinned per point. 

Interceptors return a verdict with one of three decisions; allow, deny, transform. On the wire, an escalation looks like this:

{ "decision": "deny", "reason": "refund_over_limit", "message": "requires human approval", "approval": { "resolver": "host", "context_identity": "sha256:11f8bab5…" } }

In earlier iterations, we also had warn and escalate verdicts. Warn verdict was removed since a warning is an allow carrying warnings, because warning is metadata, not control flow. An escalation is now modeled as a deny carrying an approval block, denied as-is unless the approval seam lifts it. That construction removes a failure mode outright. An unresolved escalation used to be a state the host had to remember not to proceed on; now it is simply a deny. Fail-closed is a property of the type system, not a code path someone has to maintain. 

Host obligations are the half that frameworks usually leave undefined, and they are normative here. A deny at pre_tool_call means the tool is not invoked. A deny at post_tool_call means the result is discarded and never enters agent state. A host that cannot build a valid context, cannot reach an interceptor, times one out, or receives a malformed verdict must synthesize a deny with a reserved machine-readable reason. The opening incident cannot occur on a conformant host: the crashing guard becomes a deny, and the record says so. 

Every emission also produces an InterceptionRecord, which is payload-free by design. It carries the verdict projection, which interceptor decided, the composition profile, the sequence number, and content identities computed before and after enforcement. It never carries customer content. You can export the full audit trail of an agent’s decisions without exporting a single prompt.

The approval that can’t be replayed

The approval block above carries a context_identity: a SHA-256 over the canonical JSON of exactly what the approver was shown. The resolution must echo that identity byte-for-byte. This is the difference between approving an action and approving a session.

Approval binds to content, not to a session. Change the content, and the approval does not transfer.
Approval binds to content, not to a session. Change the content, and the approval does not transfer.

In our demo suite, a support agent tries to refund $840 against a $500 cap. The guard escalates; a human sees the exact call (tool, arguments, identity) and approves. The refund executes, and the record binds the approval to that identity. Then the demo replays the approval against a mutated call: issue_refund for $8,400, claiming the earlier authorization. Different content, different identity, and the deny stands. We run this same scenario across eight frameworks: LangGraph, the OpenAI Agents SDK, Microsoft Agent Framework, Semantic Kernel, LlamaIndex, CrewAI, the Claude Agent SDK, and a bare reference host. Every one of them produces the identical 20-row decision stream, with the identity probe byte-identical across the Python, .NET, and TypeScript SDKs. One contract, one behavior, provable.

Doesn’t a hook layer slow everything down?

The enforcement seam itself is cheap. During the Microsoft Agent Framework integration review, the per-run overhead of the contract’s machinery (identity allocation, gate consultation, scope management) was measured at roughly a microsecond per run and a microsecond per streamed update on commodity hardware: noise against any model call. The honest cost lives elsewhere, and we will name it: fully fail-closed streaming means buffering. A host that guarantees no token egress before the output verdict cannot also give you first-token latency during enforcement. The spec supports a declared bounded-exposure incremental mode for hosts that need streaming, with the exposure bound stated in the conformance claim; buffered is the default, because it is the only mode with zero exposure. 

Policy evaluation is also cheap when it is in-process. The first policy runtime built on the contract originally shelled out to an external policy engine per decision: 26.8 milliseconds per evaluation, dominated by process spawn. Moving Rego evaluation in-process brought a warm evaluation to 0.32 milliseconds, measured as best-of-five over the same policy pack on the same hardware, with activation amortized after roughly twenty decisions. Your policy engine shouldn’t be the slow part of your agent, and it doesn’t have to be.

What Agent Hooks doesn’t protect against

Agent Hooks is a cooperative contract, not a security boundary. The host framework is fully trusted: interceptors run in-process with full data access, and registering an interceptor is equivalent to granting it write access to every action the agent takes. A hostile or buggy host can skip points or ignore verdicts, and the conformance kit can’t detect that. There’s no complete-mediation claim: a framework may expose direct tool execution or background paths that never reach pre_tool_call, so the contract makes coverage testable, not automatic. Server-side tool execution (hosted code interpreters, service-managed tools) can’t be intercepted at the tool seam at all; it surfaces at post_model_call, and conformant hosts document exactly that. The threat model in the spec says all of this in normative language. A hook layer governs what a cooperating framework does; containing hostile or untrusted code is a sandbox’s job, and Agent Hooks isn’t a sandbox. If someone tells you otherwise, they are selling something.

Proof it survives contact

A contract is worth what its enforcement survives. The Microsoft Agent Framework integration went through five maintainer review rounds, and the maintainers didn’t take our claims on faith: they reproduced real fail-open paths, including a retrying middleware that defeated persistence ownership and a drained-and-discarded attempt that persisted before any verdict existed. Every finding was fixed with a regression test that fails when the fix is reverted. We’ll be honest: the hardest part of this project wasn’t writing the spec; it was watching skilled reviewers falsify our own “this is fail-closed” claims, twice, and rebuilding until they couldn’t. 

That discipline is what the conformance kit packages. Forty-seven scripted scenarios drive a host through the contract: denies that must stop actions, transforms that must be applied, crashes that must become denies, approvals that must bind to content. A conformance claim is a declared surface plus the report. There is no certification theater, just results you can re-run. Two hosts hold certified claims today: the Agent Control Specification policy runtime and Microsoft Agent Framework’s core implementation, which passes 47 of 47 applicable scenarios and discloses its one non-default behavior (terminating the run on enforcement-layer failure) as a declared posture rather than papering it over. The .NET implementation is in review with the same test discipline: 84 tests, every enforcement property pinned.

Try it

pip install agent-hooks-sdk # Python npm install @responsibleai/agent-hooks # TypeScript (napi, prebuilt) cargo add agent-hooks-sdk # Rust dotnet add package ResponsibleAI.AgentHooks # .NET go get github.com/responsibleai/agent-hooks/sdk/go/agenthooks

The spec, the conformance kit, the documentation, and all five SDKs live in the agent-hooks repository at github.com/responsibleai/agent-hooks. If you’re on Microsoft Agent Framework, the feature is in core behind the agent-hooks extra today. The eight-framework demo suite, including the replay scenario above, ships with runnable, deterministic scripts that need no API keys.

An open contract needs more than one author

AGENT-HOOKS-0.1 is versioned, the schemas are published, changes go through public proposals, and the conformance kit is the arbiter of what “supported” means. We built reference implementations in five languages so that no single runtime defines the contract, and we encourage other implementations. If you maintain a framework and want the conformance report with your name on it, the harness interface is four methods, and we’ll do the integration work with you. If you build controls and the contract is missing a seam you need, the proposal process is open. We’d love to hear from both sides.

FAQs

Why not just use each framework’s middleware?

Because middleware answers “where can code run,” not “what must happen when it says no.” The contract’s value is the normative half: deny stops the action, crashes become denies, approvals bind to content, records are payload-free. Middleware is how hosts implement it; the contract is what makes the result verifiable.

Three verdicts seems small. Where are warn and escalate?

They’re encoded, deliberately: warn is allow plus warnings; escalate is deny plus a liftable approval. Five verdicts means five states hosts can mishandle; three with fail-closed composition means an unresolved anything is a deny.

What happens under streaming?

By default, everything buffers until the output verdict: zero exposure, at an honest latency cost. Hosts that need incremental release declare a bounded-exposure mode in their conformance surface, with the bound stated. What no conformant host may do is stream first and enforce later while claiming otherwise.

Can a malicious host just lie?

Yes. See the threat model section: the host is trusted, and this is a contract, not a sandbox. What the contract changes is that a cooperative host’s claims become testable, and a gap becomes a conformance finding instead of an incident.

Is this Microsoft only?

No. The spec and SDKs are MIT-licensed under an open organization; the first certified consumer is an independent policy runtime, and the same scenario suite runs on eight frameworks from six vendors. Microsoft Agent Framework is the first framework to ship it in core; the contract is written, so it will not be the last.

The post Your agent’s guardrails have a bypass appeared first on Command Line.

Read the whole story
alvinashcraft
55 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

How Uno Platform uses .NET, MCP, and AI to build high quality apps

1 Share

This is a guest post by Sam Basu. Sam is a technologist, author, speaker, Microsoft MVP and Developer Advocate for Uno Platform.

If you’ve spent any time building software in the last couple of years, you’ve felt the shift. AI is no longer a novelty sitting on the sidelines – it’s right there in the editor, the terminal, the build pipeline. And for .NET developers, this moment is particularly exciting. The ecosystem is deep, the tooling is stellar, and AI just keeps getting better at navigating both.

But raw AI power and grounded, contextual AI are two very different things. An AI agent will happily write you a settings page for a cross-platform .NET app. It will compile. It will pass review if you only read it. And it will still be wrong in ways you cannot see until the app is running in front of you. Closing that gap is the problem we set out to solve at Uno Platform – you can now build cross-platform .NET apps in browser with AI; give it a try @ https://platform.uno/.

The developers who will get the most out of this era aren’t the ones prompting the hardest – they’re the ones giving AI the right context to actually do the job well. The focus is on quality – how can we provide AI all the guardrails to be successful and be able to validate its own work, and tooling that makes .NET developers productive from the start. Let’s unpack.

Why MCP, and why we ended up with two servers

The obvious first move is context stuffing: shovel the docs into the prompt, add a long instructions file, hope for the best. It fails for a reason that is clear in hindsight. Documentation is large, the useful slice is small and query-dependent, and no amount of prompt real estate substitutes for the agent being able to look something up at the moment it has the question.

Model Context Protocol solves the lookup problem. It does not solve the verification problem. Knowing what the API should be does not tell an agent whether the layout it just wrote actually renders. Those are two different jobs with two different lifetimes, and that distinction is why we ended up with two servers rather than one.

The split we landed on maps to those two lifetimes.

The docs server: grounding

The docs server is publicly hosted at https://mcp.platform.uno/v1, speaks HTTP, and is stateless. It answers what is true about this framework right now – a question whose answer changes when we ship, not when your app runs.

  • uno_platform_docs_search – search official documentation and return the most relevant results
  • uno_platform_docs_fetch – fetch a full documentation page as markdown
  • uno_platform_agent_rules_init – initialize the agent session with rules for working against a running app
  • uno_platform_usage_rules_init – load common API usage rules

It also ships two prompts: /new to scaffold an app with current best practices, and /init to prime an existing conversation before adding a feature to an existing codebase.

The design property that matters is that this server is versioned with our documentation, not with the developer’s SDK. Correct a doc page and every agent everywhere gets the correction on its next call. That is a very different maintenance story from shipping guidance inside a NuGet package, and it is the main reason we host it rather than distribute it.

The app server: eyes and hands

The app server is the opposite in every dimension. It ships as a .NET tool launched over stdio, runs on the developer’s machine as a bridge to the Uno DevServer, is stateful, and belongs to exactly one session. It answers what is actually happening right now.

It gives an agent four capabilities. It can run the app – uno_app_start launches in debug mode with Hot Reload enabled, so the agent controls the whole lifecycle rather than waiting for a human to press F5. It can seeuno_app_get_screenshot for pixels, and uno_app_visualtree_snapshot for an XML snapshot of the visual tree. It can actuno_app_pointer_click, uno_app_key_press, uno_app_type_text, and uno_app_element_peer_action to invoke automation peers directly. And it can check itselfuno_health reports the status of the bridge and its connection, because an agent that cannot tell “the app is broken” from “my connection dropped” will confidently debug the wrong thing.

MCP tool list showing Uno app and docs servers registered in an IDE

Both servers, side by side, as the agent sees them.

The visual tree tool is the one that earns its keep. Screenshots tell a model that something looks wrong; the XML tree tells it which element is at fault and what its properties are. Pixels are for detection, structure is for diagnosis, and an agent needs both.

There is one detail in that tool list worth calling out: read the description on uno_app_pointer_click and it says prefer uno_app_element_peer_action. That preference lives in the tool description itself rather than in documentation nobody loads, because coordinate clicking is brittle across window sizes and DPI while automation peers are stable. More on why that matters below.

Building it: the MCP C# SDK in production

Both servers are written in C# on the official MCP C# SDK, which Microsoft maintains in collaboration with the community. Two things we would tell any .NET team starting the same work.

Pick your transport from your topology. The docs server is HTTP because it is a hosted multi-tenant service that needs OAuth. The app server is stdio because it is a child process on one developer’s machine talking to one running app. The topology decides the transport; there is not much of a choice to agonize over once you have written the constraints down.

Your tool definitions are a permanent tax on the context window. Every tool name, description, and input schema is loaded before the model does any work. Our docs server costs about 6.4k tokens and the app server about 1.5k – for comparison, the built-in GitHub MCP server in the same session costs about 5.2k. That is real budget spent before a single question is answered, and it is why terse, high-signal tool descriptions are not a style preference.

Copilot CLI listing MCP servers with transport, token cost, and auth

The same servers in GitHub Copilot CLI. Note the token cost per server.

That second point has a corollary: tool descriptions are prompts, not documentation. A tool the model never selects may as well not exist, and the only lever you have over selection is the wording. This is why uno_app_pointer_click explicitly tells the model to prefer the automation-peer tool instead – that is not documenting a preference, it is steering a decision at the moment it is made.

Generating code and functionality verification are different problems

Here is the hot take: AI can write UI code faster than any human team, and it cannot tell whether what it wrote is correct. As agentic workflows become normal, that asymmetry is the bottleneck. Generation got cheap. Verification did not.

Web developers already solved their half of this. Playwright drives a real browser, so an agent working on a web app can check its own work. There has been no equivalent for a native cross-platform .NET app running on Windows, macOS, Linux, iOS, Android, or WebAssembly – the app is a black box the moment it launches.

The app server is our answer to that: Playwright-style UI automation for .NET apps. The agent writes a change, the app hot reloads, the agent takes a screenshot, reads the visual tree, clicks through the flow, and decides for itself whether the change did what was asked. When it did not, the agent fixes it before handing anything back.

Code is cheap. Software is not. This is how you hold both truths at once.

Skills: giving the agent the “how”

MCP tools give an agent the what. They do not say when to reach for which one, or in what order, or what “done” looks like. That is what Skills are for.

The cooking analogy holds up well here. MCP tools are ingredients – atomic, each does one thing. Skills are recipe cards – the reusable instructions for combining ingredients into something worth eating. The agent is the cook, choosing a recipe and adapting it to what is actually in the kitchen.

Our Skills library is organized by the thing you are actually doing: MVUX state and feeds, navigation, theming, the Uno Toolkit controls, and testing. The one that closes the loop is uno-testing-ui, which automates UI testing through the app server – the Skill knows the order to drive the tools in, so the agent does not have to work it out from first principles every session.

Grounded documentation, a live app it can inspect, and curated procedure for the workflows that matter: that combination is what we mean by contextual AI.

Uno Platform Skills listed as toggleable plugins inside an AI agent session

Skills install as plugins, available to any MCP-compatible agent.

What it adds up to

The most interesting thing we built with all of this is not a feature list, it is a compiler running where a compiler has no business running.

Uno Platform Studio 3.0 generates a full cross-platform .NET app entirely in the browser. Behind the prompt box, a specialized agent orchestrated by Microsoft Agent Framework plans and executes the work across parallel steps and multi-turn conversations. A full Roslyn workspace then compiles what the agent writes, loads the generated assemblies, resolves NuGet changes, and hot reloads the result into the running app – all in the browser, while you watch. The docs server keeps the agent’s knowledge current. The app server lets it check its own work. The Skills keep it on the rails.

That is Roslyn, Microsoft Agent Framework, and the MCP C# SDK doing work that would have been a research project a few years ago, and the entire stack is .NET.

Uno Platform Studio generating a CRM dashboard app in a browser with an agent panel

Prompt on the right, compiled and running .NET app on the left. Not a mockup.

The practical consequence for a team is that the agent stops being a fast typist. It knows your design system, it validates its own output against a running app, and it follows workflows you chose. That is a different proposition from writing code faster.

The generated .NET app is fully interactive in the browser, along with page navigation and Previews to work on app UI in isolation. Developers can iterate on app UI with the Agent or manually with Hot Design in the browser – the changes are immediately visible with Hot Reload. There is no barrier to entry – developers can start in the browser, iterate on app UI with Agent or Hot Design, and drop down to local IDE/CLI with same tools, when ready.

Uno Platform Studio in edit mode

Why we work upstream

None of this would be buildable on a foundation we could not influence, and that is the honest reason we invest where we do.

We co-maintain SkiaSharp alongside Microsoft’s .NET team. SkiaSharp is the 2D graphics API underneath a large share of .NET charting, custom controls, and data visualization – it is built on Google’s Skia, the same engine in Chrome and Android – and it is what Uno Platform renders with. Becoming a co-maintainer formalized years of investment ahead of SkiaSharp 4.0, the largest release the project has had in years.

We also work directly on the .NET runtime through a formal collaboration with the Microsoft .NET team, contributing to .NET for Android and .NET for iOS bindings and to AOT in .NET 10.

The pattern is the same one this whole post describes: the further upstream you fix something, the more people never have to think about it again.

Wrap up

If you are building an MCP server for your own .NET stack, the two things we would pass along are these. Split your servers by lifetime, not by feature – knowledge that changes when you ship does not belong in the same process as state that changes when the app runs. And spend real time on your tool descriptions, because they are prompts, and a tool the model never selects may as well not exist.

The rest is ordinary .NET. The MCP C# SDK, Roslyn, Microsoft Agent Framework, and a graphics stack we help maintain, doing work that is anything but ordinary.

Try the Uno Platform MCP servers at aka.platform.uno/mcp.

The post How Uno Platform uses .NET, MCP, and AI to build high quality apps appeared first on .NET Blog.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Global Secure Access – Now with Windows Update support!

1 Share

Last Updated on August 27, 2026 by Michael Morten Sonne Introduction An interesting improvement is making it´s way…

The post Global Secure Access – Now with Windows Update support! first appeared on Blog - Sonne´s Cloud.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

VSLive! @ Microsoft HQ: Developer Takeaways and Must-Watch Sessions

1 Share

VSLIVE MSHQ recap blog banner image

VSLive! @ Microsoft HQ 2026 was one of the most energizing VSLive! events I’ve attended. What stood out was the level of engagement over five days of learning and conversation from July 27 through July 31. Developers came ready to dig into the future of Visual Studio, .NET, and AI-assisted software development, and many Microsoft engineers and product managers were there to have those conversations directly. 

If you weren’t able to join us in Redmond, there’s still plenty you can take away from the event. The Microsoft-led sessions were recorded, so you can catch up on many of the topics that generated some of the biggest conversations during the week. 

AI is becoming part of the entire developer workflow 

If there was one theme that ran through VSLive! @ Microsoft HQ, it was AI. 

The conversation has moved well beyond code completion. Developers are thinking about how AI can help with planning, debugging, modernization, testing, code review, agents, and workflow automation. 

What I found especially important was how practical these conversations were. Developers want to know what works today, what can be trusted in real development environments, how teams can adopt AI responsibly, and where human judgment still matters. 

Great AI experiences still need great developer fundamentals 

One of the biggest takeaways from the week was that developers aren’t asking us to choose between AI innovation and the fundamentals of a great development environment. 

They want both. 

Speed, stability, predictability, debugging, testing, and usability still matter enormously. AI becomes more valuable when it builds on a development experience developers already trust. 

That reinforces why Visual Studio remains so relevant as development changes. Sessions covering GitHub Copilot in Visual Studio, debugging, productivity, MCP, and AI workflows showed how the IDE can increasingly serve as an orchestration point for modern software development while continuing to support the fundamentals developers depend on every day. 

.NET modernization is happening alongside AI adoption 

The excitement around AI doesn’t mean developers are leaving their existing applications behind. Quite the opposite. 

We saw strong interest in AI-enabled .NET applications, Aspire, Blazor, .NET MAUI, WinForms, SQL integration, and C#. But many developers are also starting from applications built on .NET Framework 4.7 or 4.8, Web Forms, WinForms, or codebases that have grown over many years. 

For those teams, the challenge is practical: How do you move toward .NET 8 or .NET 10 when time and staffing are limited and disruption needs to be kept under control? 

That makes modernization guidance and tooling an important part of the same conversation. Developers want to explore what’s next while continuing to improve the applications their businesses depend on today. 

VSLive! @ Microsoft HQ 2026 sessions: hot takes 

If you missed VSLive! @ Microsoft HQ, or attended and want to revisit what you learned, the recorded sessions are a great way to keep on learning. 

You can explore select VSLive HQ 2026 sessions on the Visual Studio YouTube channel, including these already trending videos: 

Improving Performance in .NET Applications

Performance remains one of the fundamentals developers care deeply about. If you’re looking to improve how your .NET applications perform, this is a good session to add to your watch list. 

Explore the Future of ASP.NET Core & Blazor in .NET 11

For developers building modern web applications, this session explores what’s ahead for ASP.NET Core and Blazor in .NET 11. 

Modernizing .NET Applications

Modernization was a strong, recurrent theme during VSLive! @ Microsoft HQ 2026. If you’re working with an established .NET codebase and thinking about what comes next, this session is especially relevant. 

Everything You Need to Know About the Latest in C#

C# continues to evolve alongside .NET. This session is a chance to catch up on the latest developments and think about how they apply to the applications you’re building. 

SQL MCP Server: Bringing AI Agents to Your SQL Data

This session brings together two themes that generated a lot of interest: AI and MCP. It’s a practical example of how developers are beginning to think beyond AI-assisted coding and toward agents that can work with the systems and data behind their applications. 

Whether you’re exploring AI, modernizing an existing application, improving performance, or staying current with .NET and C#, there’s a lot here you can bring back to your own projects and teams. 

Keep learning with VSLive! 

There are two more opportunities this year to connect with developers, learn from experts, and get hands-on with the technologies shaping modern development. 

VSLive! San Diego 

September 14-18, 2026 

Bahia Resort Hotel, San Diego, CA 

VSLive! San Diego is another opportunity to step away from the daily backlog, sharpen your skills, and spend time beachside in the warm Southern California sun with developers tackling many of the same challenges you are. 

VSLive! / Live! 360 Tech Con 2026 

November 15-20, 2026 

Royal Pacific Resort – Universal Orlando 

VSLive! / Live! 360 Tech Con 2026 offers another opportunity to close out the year with practical learning, technical conversations, and connections across the developer community. 

If you’re a Visual Studio subscriber and considering joining us at one of these events  Sign in to my.visualstudio.com to access your VSLive! discount code. 

Thanks to everyone who joined us in Redmond, asked questions, shared feedback, and spent time with our teams. Those conversations matter, and they help us shape what comes next. 

 

The post VSLive! @ Microsoft HQ: Developer Takeaways and Must-Watch Sessions appeared first on Visual Studio Blog.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

What is cloud database vendor lock-in? The 3 types explained (and how to audit yours)

1 Share

Cloud database vendor lock-in is one of the most expensive surprises in enterprise infrastructure — not because the dependency is hidden, but because most teams never quantify it before committing to a platform. Each distinct type – engine lock-in, feature lock-in, and operational lock-in – accumulates differently, costs a different amount to exit, and requires a different conversation before you migrate.

This article breaks down exactly what each type means, which cloud providers create the deepest dependencies, and introduces a four-question framework for measuring your own lock-in — before a pricing change, service deprecation or organizational shift forces the issue.

Every time you move a database workload to a managed cloud service, you (and your team) are accepting some form of vendor dependency (‘lock-in’). However, what normally isn’t accepted is exactly what you are committed to, how deep it runs, and what it would actually cost to undo. It’s a conversation so many teams never have before committing to a cloud migration – and it’s an important, potentially costly, one to miss.

Why does this happen – and what are the consequences?

That conversation gets deferred because the managed service is genuinely useful, so there’s pressure to migrate to it. Plus, this vendor lock-in problem you’re hearing about seems so insignificant – maybe even hypothetical – next to the operational problem you and the team are currently struggling through.

However, it very much stops feeling hypothetical when your provider raises prices, changes a service behavior your application depended on or, worse still, the needs of your organization change enough that the cloud service is no longer required.

The key stat

Gartner’s 2025 Magic Quadrant for Cloud Database Management Systems (November 2025) found enterprise cloud database adoption accelerating, with distributed SQL among the most actively evaluated categories.

Redgate also reported similar findings in their special Cloud Migration Divide report (part of the 2026 State of the Database Landscape survey).

This article makes the lock-in conversation specific, explaining the three distinct types of cloud database dependency. Understanding which type you are accumulating, and how much of it, is the kind of clarity you need before making a big migration decision – not after, when it’s too late.

What are the 3 types of cloud database lock-in?

Engine lock-in, feature lock-in, and operational lock-in. These are the three distinct types of cloud database lock-in, but what do they each mean? What are the differences between them? Let’s find out.

Engine lock-in (the least dangerous type)

Engine lock-in means dependency on a specific database product, not on a specific vendor’s infrastructure. The migration is operationally painful – it involves downtime planning, data transfer, connection string updates, and validation. However, it’s technically straightforward, because the engine runs the same code wherever it runs. 

It’s also, in practice, often the least expensive to exit. If you’re running PostgreSQL on Amazon RDS, the database engine is standard, community-maintained PostgreSQL. Your schemas, stored procedures, queries, and application connection strings work against any PostgreSQL instance, anywhere.

So, if you decide to migrate to Azure Database for PostgreSQL, Google Cloud SQL for PostgreSQL, or a self-hosted instance, the technical barriers are low. 

The reason this matters less than it appears is that organizations rarely remain at engine-level dependency once they begin using a managed service. The managed service features that make it worth choosing over self-hosted are almost always provider-specific.

Feature lock-in (where the actual costs accumulate) 

Feature lock-in occurs when you build application behavior that depends on capabilities specific to one provider’s implementation of a database engine. The data is portable, but the behavior is not. 

Amazon Aurora is the clearest example. Aurora presents itself as MySQL-compatible and PostgreSQL-compatible, and at the protocol level, that is largely accurate. But Aurora’s underlying storage layer is not MySQL or PostgreSQL storage – it’s a distributed log-based storage system engineered by AWS that exists nowhere outside of AWS.

This architecture delivers genuine performance advantages: up to five times the throughput of standard MySQL on equivalent hardware, up to 15 read replicas (compared to five for standard RDS), and lower replication lag under write-heavy workloads. 

Those advantages come from Aurora-specific design. For example, Aurora’s exclusive backtrack capability, which lets you rewind the database to a previous point without restoring from a backup. Then there’s Aurora Global Database’s cross-region replication, which has different semantics from standard PostgreSQL streaming replication.

And finally, Aurora Serverless v2’s autoscaling behavior, which responds to traffic changes in fine-grained increments that have no equivalent on other platforms. 

The point is simple: if you build operational runbooks, application logic, or disaster recovery procedures that depend on any of these behaviors, you have feature lock-in. Migrating your data from Aurora to standard PostgreSQL is technically straightforward.

Replicating Aurora’s performance characteristics and operational behaviors on a different platform, however…that’s a different engineering project entirely,  and one that often takes longer than the data migration itself. 

Operational lock-in (the most underestimated type) 

Operational lock-in is the accumulation of integrations, monitoring configurations, automation scripts, IAM policies, VPC configurations, credential management approaches, alerting setups, and institutional knowledge that builds up around a specific managed service over time. It’s both the least discussed type and the most expensive to exit. 

Let’s take, for example, a database that has been running on Amazon RDS for three years. Typically, it’ll have CloudWatch dashboards tuned to its specific metrics, AWS IAM roles configured for access control, automated backup jobs defined in RDS parameter groups, performance insights reports that the team references daily, and staff who know how to interpret RDS console outputs.

None of that is the database itself. Instead, it’s all the operational context around the database – and crucially, it doesn’t transfer to another platform. When organizations underestimate migration costs, this is nearly always where the discrepancy comes from.

There’s also the time cost to consider. The data migration may have only taken two weeks, but the operational infrastructure rebuild takes three months. Retraining the team to work with a different management interface and toolset? That’s another two months. These costs are practically invisible until the migration is actually underway, so they rarely surface in initial vendor comparison exercises.

Simple Talk is brought to you by Redgate Software

Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.
Discover how Redgate can help you

Cloud vendors and lock-in: a guide

Let’s now look at some cloud vendors and how each handles lock-in. Here, we’re focusing on Amazon Aurora, Azure SQL Managed Instance, and Google Cloud Spanner.

Amazon Aurora 

Amazon Aurora genuinely delivers on its performance claims before it creates its dependencies. AWS announced Aurora DSQL at re:Invent 2024, positioning it as a serverless distributed SQL database with 99.999% multi-region availability and active-active architecture.

By Q1 2026, Aurora DSQL had reached general availability across four AWS regions, in direct competition with Google Cloud Spanner and CockroachDB Dedicated

The technical claims are substantiated. Aurora DSQL uses optimistic concurrency control rather than the traditional pessimistic locking approach, which eliminates lock contention in distributed write scenarios and reduces cross-region write latency compared to traditional multi-version concurrency control (MVCC) implementations. These are major benefits for organizations running globally (geographically) distributed applications with high write throughput.

The lock-in comes from what you give up to get these capabilities. Aurora DSQL’s documentation is explicit: the service doesn’t support explicit database locks (because it uses optimistic concurrency control), foreign keys, temporary tables, or certain PostgreSQL extensions.

Additionally, if your application contains SELECT FOR UPDATE statements or advisory locks, it needs to be refactored before using Aurora DSQL. Every application pattern you build to work around these constraints is application logic specific to Aurora DSQL’s behavior. 

AWS’s own documentation describes Aurora as creating ‘a different vendor lock-in by providing unmatched ROI.’ That’s a remarkably candid statement, but it’s true: the return on investment is real. In turn, so is the lock-in, which only deepens with every Aurora-specific feature you adopt.

Azure SQL Managed Instance

Azure SQL Managed Instance (MI) occupies a different position in the lock-in landscape, marketed on proprietary performance features rather than just compatibility. 

Microsoft’s documentation positions it as being nearly 100% compatible with on-premises SQL Server, designed specifically for lift-and-shift migration of existing workloads. That compatibility is real and meaningful for organizations migrating complex SQL Server environments to the cloud. 

The lock-in story here is ecosystem lock-in rather than feature lock-in. Instead of offering exotic capabilities that can’t be replicated elsewhere, Managed Instance integrates tightly with Microsoft Entra ID for authentication, Azure Monitor for observability, Azure Blob Storage for backup destinations, and the Azure networking stack for connectivity. For organizations already invested in the Microsoft ecosystem, this makes it a no-brainer to use MI.

There are also documented gaps between Managed Instance and on-premises SQL Server worth understanding before migration. Microsoft’s T-SQL differences documentation lists specific behaviors around linked servers, which in MI are limited to a small set of Azure targets and SQL Server instances, with no support for external file systems or other relational databases, backup and restore operations, certain replication scenarios, and distributed transactions. These gaps have narrowed with each release but remain relevant for complex on-premises environments. 

For organizations coming from SQL Server, Managed Instance is often the most viable cloud path because the alternatives require more extensive re-platforming. Said organizations just need to remember that choosing Managed Instance means committing to a deepening integration with the Azure services stack that surrounds it.

Google Cloud Spanner 

Google Cloud Spanner is a globally-distributed relational database built on Google’s proprietary TrueTime infrastructure, atomic clocks, and GPS receivers distributed across Google’s data centers. It represents the most complete form of cloud database lock-in in commercial use – and is quite honest about it.

Cloud Spanner provides external consistency across regions – delivering horizontal scaling with strong consistency without the consistency compromises that typically accompany distributed systems. 

The SQL dialect Spanner uses is not standard ANSI SQL. It diverges from PostgreSQL and MySQL on data types, functions, and query semantics in ways that make application code written for Spanner non-portable to other databases without meaningful rewriting.

Furthermore, Spanner doesn’t support foreign keys in the same way traditional RDBMS products do. Its pricing model, based on nodes or processing units, is Spanner-specific, so doesn’t align with other database services. Broadly speaking, organizations using Spanner know and accept this. The distributed consistency properties Spanner delivers aren’t available at the same operational simplicity as any other platform. 

For example, CockroachDB and YugabyteDB offer comparable consistency models but require more operational involvement. Similarily, Aurora DSQL is competitive for certain workloads but is newer, with more constraints. Spanner’s specific – and prominent – technical position is chosen by organizations choose because the alternative is building the distributed consistency infrastructure themselves. 

The organizations that run into problems with Spanner are the ones who adopted it for a workload that didn’t actually require its distributed consistency capabilities. Before realizing this, they’d already built application logic specific to the platform. The result is SQL dialect differences and the absence of standard foreign key behavior – issues that compound over time.

How do you measure cloud lock-in? The four-question framework you should use

Before adopting a cloud database service or auditing one you already rely on, these four questions make the dependency concrete enough to inform a real decision.

1. What does a complete data export look like – and can you run it today? 

For standard PostgreSQL on RDS, you can run pg_dump and restore the output anywhere PostgreSQL runs. For Aurora DSQL with optimistic concurrency control behavior embedded in your application code, the data export is straightforward, but the behavioral compatibility is not. Running through this exercise before you need to do it tells you the actual complexity.

2. How much application code depends on provider-specific behaviors? 

Stored procedures that reference Aurora-specific system variables. Every monitoring integration that reads CloudWatch-specific metrics rather than standard database views. Each authentication flow that uses AWS IAM database authentication rather than standard credentials. These are all examples on lock-in ‘surfaces.’

Why is this important? Well, put simply, listing them all gives you an estimation of migration cost – even if migration isn’t a current priority. You’ll know if the list is too long before it becomes relevant…and costly.

3. How many other services does this database integrate with, and are those integrations portable? 

What’s the significance of a database that feeds an event stream to a cloud-native message queue, triggers cloud-native functions on changes, and is monitored through a cloud-native observability platform? Answer: it has accumulated operational lock-in well beyond the database itself.

Moving the database to a different provider doesn’t move any of that integration infrastructure, either. Understanding the full integration surface is necessary for any realistic migration cost estimate.

4. You need to move in 12 months. How long will it take, and how much will it cost?

You need to be concrete here. Estimate the data migration time, the operational infrastructure rebuild, the integration rework, and the time spent on team retraining. If, once added up, the number is 18 months and many engineers, you have useful information regardless of whether migration is currently on the roadmap. Information that should inform every significant cloud database platform decision. 

In conclusion: navigating cloud database lock-in

The goal of this framework is not to argue against cloud database lock-in categorically. For many organizations, deep integration with a single provider’s services is the right decision.

It might be that Aurora’s performance and operational simplicity are a huge asset to you. Or perhaps it’s Azure SQL Managed Instance’s SQL Server compatibility, removing (expensive) migration friction for your organization. And then there’s Spanner’s global consistency capabilities, which are unique. 

In short, it’s not a mistake to choose a ‘locked-in’ service. It is a mistake, however, to choose one without making the trade-off explicit and understanding what exit actually looks like. Organizations with a full understanding of the depth of their lock-in are in a much better position to negotiate with their provider, plan for scenarios that require changes, and evaluate new options as they emerge.

On the other hand, organizations that discover their lock-in during an emergency simply do not have these options. 

Measure before you build. The cost of that clarity is a few hours of architecture discussion – significantly less hefty than the cost, and consequences, of discovering it during an unplanned re-platforming event.

How to use Redgate Flyway as a multi-database migration system

Learn how to use Flyway to do a single-batch, multi-database migration, comprising SQL Server, Oracle Cloud, PostgreSQL, MySQL and SQLite databases.
Read the guide

FAQs: Cloud database vendor lock-in

1. What are the three types of cloud database lock-in?

The three types are engine lock-in (dependency on a specific database product, such as PostgreSQL), feature lock-in (dependency on provider-specific capabilities like Aurora’s backtrack or Spanner’s TrueTime consistency), and operational lock-in (dependency on the surrounding infrastructure — IAM policies, monitoring dashboards, automation scripts, and institutional knowledge — that accumulates over time).

2. Which type of cloud database lock-in is hardest to exit?

Operational lock-in is the most expensive and underestimated to exit. While a data migration may take two weeks, rebuilding the operational infrastructure — monitoring, access control, automation, alerting — on a new platform typically takes months. Team retraining adds further time. These costs rarely appear in initial vendor comparison exercises.

3. Does Amazon Aurora create vendor lock-in?

Yes. Although Aurora presents itself as MySQL- and PostgreSQL-compatible, its underlying storage layer is proprietary to AWS. Features like Aurora Serverless v2 autoscaling, Aurora Global Database cross-region replication, and the backtrack capability have no direct equivalents on other platforms. AWS’s own documentation acknowledges Aurora creates “a different vendor lock-in by providing unmatched ROI.”

4. How do you measure cloud database lock-in before migrating?

Use a four-question framework: (1) Can you run a complete data export today, and is the output portable? (2) How much application code depends on provider-specific behaviours? (3) How many integrations surround the database, and are they portable? (4) If you had to migrate in 12 months, what would it realistically cost in time and engineering resource? Answering these concretely before migration surfaces hidden costs.

5. Is Google Cloud Spanner more locked-in than other cloud databases?

Spanner represents the most complete form of cloud database lock-in commercially available. Its SQL dialect diverges from PostgreSQL and MySQL on data types, functions, and query semantics. It is built on Google’s proprietary TrueTime infrastructure (atomic clocks and GPS receivers) that exists nowhere outside Google’s network. Organisations typically choose Spanner knowing and accepting this, because its global consistency properties are not available at the same operational simplicity elsewhere.

6. Can you avoid cloud database lock-in entirely?

Avoiding lock-in entirely usually means forgoing the features that make managed cloud databases valuable in the first place. The more practical goal is to make the trade-off explicit: understand which type of lock-in you are accumulating, how deep it runs, and what exit would realistically cost — so that decision is made deliberately rather than discovered during an unplanned re-platforming event.

The post What is cloud database vendor lock-in? The 3 types explained (and how to audit yours) appeared first on Simple Talk.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Angular Signals vs RxJS: Should You Replace RxJS in Real Apps?

1 Share

Angular Signals vs RxJS Should You Replace RxJS in Real Apps

TL;DR: Choose the right balance between Angular Signals and RxJS to build scalable, maintainable apps. Learn when to use each for state management, async workflows, HTTP calls, forms, and real-world architecture decisions without overcomplicating your code.

If you’ve worked with Angular for a while, you’ve probably used RxJS everywhere, sometimes more than necessary.

A typical component ends up with:

  • BehaviorSubject for UI state
  • async pipes in templates
  • .subscribe() and cleanup logic

For simple things like toggling a tab or tracking a selected item, that starts to feel like overkill.

Angular Signals change that. They make local state management feel simple again.

But once your feature grows, adding HTTP calls, debouncing, retries, or form streams, the question becomes bigger:Should Signals replace RxJS completely?

Short answer: No. And trying to do that often makes things worse.

Why developers want to replace RxJS

RxJS in Angular has always been powerful, but it can also become noisy.

Common pain points include:

  • Too many Subject and BehaviorSubject wrappers
  • Overuse of observable state for simple UI flags
  • Nested streams that are hard to debug
  • Manual subscription concerns
  • Template clutter with multiple async pipes
  • State services that expose everything as $

A typical Angular component often starts like this:

import { BehaviorSubject } from 'rxjs';

export class TabsComponent {
  private readonly selectedTabSubject = new BehaviorSubject('overview');

  readonly selectedTab$ = this.selectedTabSubject.asObservable();

  setSelectedTab(tab: string): void {
    this.selectedTabSubject.next(tab);
  }
}

For a simple Angular component state, this feels heavier than necessary.

Signals solve that exact pain:

import { signal } from '@angular/core';

export class TabsComponent {
  readonly selectedTab = signal('overview');

  setSelectedTab(tab: string): void {
    this.selectedTab.set(tab);
  }
}

This comparison focuses on local UI state, not shared event streams or cases that require Observable composition.

The improvement is not only in fewer lines. The state is easier to read, easier to update, and easier to bind in templates.

But real Angular apps are not only local state. They include Angular HTTP calls, route changes, forms, WebSockets, debounced inputs, polling, cancellation, retries, and user event streams. That is where RxJS still matters.

Angular also provides @angular/core/rxjs-interop to integrate Signals with RxJS via utilities like toSignal() and toObservable(), making coexistence a practical architectural choice.

What are Angular signals?

Signals expose a current value whenever they are read. When created from Observables, an explicit initial value or undefined state may be required until the first emission.

Basic example:

import { computed, signal } from '@angular/core';

const quantity = signal(2);
const price = signal(499);

const total = computed(() => quantity() * price());

Signals are especially useful when a state has a current value, and derived values can be calculated synchronously.

No subscriptions. No async pipes. No extra layers.

Signals are a great fit for:

  • Local UI state
  • Toggles and flags
  • Selected items
  • Derived values (computed state)
  • Component view models

If your question is: What is the current value right now?

Signals are usually the right choice.

What is RxJS in Angular?

RxJS in Angular is used to work with asynchronous and event-based data streams. RxJS is about streams over time.
It powers Angular features like:

  • HttpClient
  • Form valueChanges
  • Router params
  • Event streams

Code example:

this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query =>
    this.http.get(`/api/search?q=${encodeURIComponent(query)}`)
  )
);

Here you’re not just tracking state, you’re managing:

  • time
  • async behavior
  • cancellation
  • retries

RxJS shines when you need

  • Debouncing or throttling
  • HTTP request handling
  • WebSocket streams
  • Event coordination
  • Complex async workflows

If your question is: How do values change over time?
That’s RxJS.

Angular Signals vs RxJS: The core difference

Think of it this way:

  • Signals → current value
  • RxJS → values over time

Signals are well suited to representing current reactive state and derived values, while RxJS is well suited to composing asynchronous, event-based, and time-dependent streams.

This distinction drives everything about how you design your app.

Signal example: Clean UI State

import { computed, signal } from '@angular/core';

interface Product {
  id: number;
  name: string;
  category: string;
}

export class ProductListComponent {
  readonly products = signal<Product[]>([]);
  readonly searchTerm = signal('');
  readonly selectedCategory = signal<string | null>(null);

  readonly filteredProducts = computed(() => {
    const term = this.searchTerm().toLowerCase();
    const category = this.selectedCategory();

    return this.products().filter((product) => {
      const matchesTerm = product.name.toLowerCase().includes(term);
      const matchesCategory = !category || product.category === category;

      return matchesTerm && matchesCategory;
    });
  });
}

There is no need for:

  • combineLatest
  • BehaviorSubject
  • map
  • shareReplay
  • async pipe

This is purely a synchronous state. Signals handle it perfectly.

RxJS example: Async + Time-Based Logic

import { HttpClient } from '@angular/common/http';
import { FormControl } from '@angular/forms';
import {
  catchError,
  debounceTime,
  distinctUntilChanged,
  map,
  of,
  switchMap
} from 'rxjs';

interface Product {
  id: number;
  name: string;
}

export class SearchComponent {
  readonly searchControl = new FormControl('', { nonNullable: true });

  readonly results$ = this.searchControl.valueChanges.pipe(
    map((query) => query.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) => {
      if (!query) {
        return of([]);
      }

      return this.http
        .get<Product[]>(`/api/products?q=${encodeURIComponent(query)}`)
        .pipe(
          catchError(() => of([]))
        );
    })
  );

  constructor(private readonly http: HttpClient) {}
}

Here you need:

  • debouncing
  • cancellation
  • error handling

Signals alone are not a replacement for RxJS stream operators such as debounceTime, switchMap, and retry, or for composing cancellation behavior.

Recommended architecture: Use both together

In real production apps, the best pattern looks like this:

  1. Store UI input in a Signal
  2. Convert it to an Observable
  3. Use RxJS for async processing
  4. Convert the result back into a Signal

For workflows that require RxJS operators such as debouncing, cancellation, or stream composition, a Signal → Observable → RxJS → Signal pattern can be useful.

Code example: RxJS for Fetching, Signals for Rendering

import { Component, computed, inject, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import {
  catchError,
  debounceTime,
  distinctUntilChanged,
  map,
  of,
  startWith,
  switchMap
} from 'rxjs';

interface Product {
  id: number;
  name: string;
}

interface SearchState {
  data: Product[];
  loading: boolean;
  error: string | null;
}

@Component({
  selector: 'app-product-search',
  template: `
    <input
      [value]="query()"
      (input)="query.set($any($event.target).value)"
      placeholder="Search products"
    />

    @if (loading()) {
      <p>Loading products...</p>
    }

    @if (error()) {
      <p class="error">{{ error() }}</p>
    }

    @if (!loading() && products().length === 0) {
      <p>No products found.</p>
    }

    <ul>
      @for (product of products(); track product.id) {
        <li>{{ product.name }}</li>
      }
    </ul>
  `
})
export class ProductSearchComponent {
  private readonly productService = inject(ProductService);

  readonly query = signal('');

  private readonly searchState$ = toObservable(this.query).pipe(
    map((query) => query.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) => {
      if (!query) {
        return of<SearchState>({
          data: [],
          loading: false,
          error: null
        });
      }

      return this.productService.searchProducts(query).pipe(
        map((data) => ({
          data,
          loading: false,
          error: null
        })),
        startWith({
          data: [],
          loading: true,
          error: null
        }),
        catchError(() =>
          of({
            data: [],
            loading: false,
            error: 'Unable to load products. Please try again.'
          })
        )
      );
    })
  );

  readonly searchState = toSignal(this.searchState$, {
    initialValue: {
      data: [],
      loading: false,
      error: null
    }
  });

  readonly products = computed(() => this.searchState().data);
  readonly loading = computed(() => this.searchState().loading);
  readonly error = computed(() => this.searchState().error);
}

Note: ProductService is a custom application service that wraps HTTP calls. It is referenced here only to keep the example focused on Signals and RxJS interoperability.

Why this works well:

  • Signal owns the input state
  • RxJS handles debounce, cancellation, errors, and HTTP calls
  • Signal exposes final render state to the template

This approach keeps Angular async data streams powerful without making the template observable-heavy.

Real-world use cases

1. Angular component state

Use Signals for:

  • Selected tabs
  • Modals
  • Filters
  • UI flags
import { signal } from '@angular/core';

export class LayoutComponent {
  readonly isSidebarOpen = signal(false);

  toggleSidebar(): void {
    this.isSidebarOpen.update((open) => !open);
  }
}

Replacing RxJS here usually improves readability.

2. Angular HTTP calls

Use RxJS for:

  • Request pipelines
  • Cancellation
  • Retry logic

Angular’s HttpClient returns Observables, making RxJS operators a natural fit for composing request pipelines. You can then convert the resulting Observable to a Signal for rendering, as shown below.

import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';

readonly products = toSignal(
  this.productService.getProducts().pipe(
    catchError(() => of([]))
  ),
  { initialValue: [] }
);

Note: Signals require a current value. Observables may not emit synchronously, so toSignal() supports options like initialValue, undefined, and requireSync.

3. Angular forms

Use both:

  • Signals → UI state (validity, visibility)
  • RxJS → valueChanges, async validation, autosave

Code example:

import { computed } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import {
  catchError,
  debounceTime,
  distinctUntilChanged,
  map,
  of,
  startWith,
  switchMap
} from 'rxjs';

export class UserFormComponent {
  readonly form = new FormGroup({
    email: new FormControl('', { nonNullable: true }),
    role: new FormControl('', { nonNullable: true })
  });

  private readonly emailAvailable$ = this.form.controls.email.valueChanges.pipe(
    map((email) => email.trim()),
    debounceTime(400),
    distinctUntilChanged(),
    switchMap((email) => {
      if (!email) {
        return of(null);
      }

      return this.userService.checkEmail(email).pipe(
        catchError(() => of(false))
      );
    })
  );

  readonly emailAvailable = toSignal(this.emailAvailable$, {
    initialValue: null
  });

  readonly formStatus = toSignal(
    this.form.statusChanges.pipe(startWith(this.form.status)),
    { initialValue: this.form.status }
  );

  readonly canSubmit = computed(() => {
    return this.formStatus() === 'VALID' && this.emailAvailable() === true;
  });

  constructor(private readonly userService: UserService) {}
}

The formStatus Signal is used to make the Observable-based form status easier to consume reactively in the template. It is a convenience pattern rather than a requirement for Angular forms.

Here, RxJS handles the async validation stream. Signal makes the result easy to consume in the template.

Comparison table

Area Angular Signals RxJS
Best use case Current synchronous state Async streams and events over time
Component state Excellent Often unnecessary for simple local state
Derived state Excellent with computed() Useful when sources are Observables
HTTP calls Good for final UI state Better for request pipelines
Debounce and throttle Not the primary use case Excellent
Cancellation Limited directly Excellent with switchMap
Forms Good for UI flags and derived state Strong for valueChanges
Event handling Good for simple state updates Strong for event streams
Template usage Direct Signal reads async pipe or conversion
Production architecture State layer Async stream layer

Common mistakes to avoid

1. Replacing all RxJS with Signals

Not everything is state. Many things are streams.

Keep RxJS for:

  • Router events
  • WebSockets
  • Form streams
  • DOM events

2. Overusing toSignal()

Each toSignal() call subscribes to its source Observable. Avoid repeatedly converting the same Observable, and reuse the resulting Signal where possible.

3. Ignoring initial values

A Signal created with toSignal() may return undefined until its source Observable emits. Provide an initialValue, handle the possible undefined state, or use requireSync only when the source is guaranteed to emit synchronously.

4. Using Signals for async side effects

  • Avoid using effect() as a general replacement for RxJS pipelines.
  • Use RxJS when you need stream operators such as debouncing, cancellation, retries, or asynchronous composition.

Quick decision checklist

Use Signals when:

  • You need the current value
  • State is synchronous
  • It directly drives the UI

Use RxJS when:

  • Values arrive over time
  • You need operators like debounce or switchMap
  • You’re handling async workflows

Frequently Asked Questions

Can Signals replace state libraries like NgRx?

Signals can handle a significant amount of local and shared state, but large applications may still benefit from dedicated state-management patterns or libraries when they require centralized state, predictable update patterns, selectors, effects, debugging, or other advanced capabilities.

Should services expose Signals or Observables?

Expose Signals for state and Observables for streams.

Do Signals work with modern Angular setups?

Yes. Signals integrate with Angular’s modern change-detection model, including OnPush. For zoneless applications, verify the guidance for the Angular version you are using.

Conclusion

Angular Signals don’t replace RxJS, and they aren’t meant to. They solve a different problem.

  • Signals simplify state
  • RxJS handles time and async behavior

The best Angular apps don’t choose one over the other. They use both clearly and intentionally, in the right places.

Final thought

If you’re planning a migration, don’t rewrite everything.
Start small:

  • Replace simple BehaviorSubject usage with Signals
  • Keep RxJS pipelines where they matter

That’s how you modernize Angular without breaking what already works.

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