Fransiscus Setiawan

Technical articles on AI agents, Azure, .NET, architecture, and EV charging systems from Sydney.

Dynamic Workflows in Claude Code: The Basic Concept (and What Equivalents Look Like)

Every few months the industry invents a new label for “multiple model calls that somehow cooperate,” and right now one of the loudest is dynamic workflow — especially inside Claude Code. In this post I unpack Claude Code dynamic workflows as a basic concept first — before the feature checklist. The problem is that the same two words mean different things depending on who is talking.

In Anthropic’s older engineering writing, a workflow is the less autonomous thing: fixed code paths that call models. An agent is the system that decides its own next step. In Claude Code’s newer product surface, a dynamic workflow is almost the opposite intuition for many people: Claude writes a JavaScript orchestration script, a runtime executes that script, and the script fans work out across many subagents. The plan lives in code, not in a single chat transcript.

This post is the basic concept I wish someone had drawn for me on a whiteboard. It covers what a dynamic workflow is in Claude Code, how it differs from subagents / skills / agent teams, how that maps to Anthropic’s broader “workflows vs agents” vocabulary, and what the equivalent ideas look like outside Claude — LangGraph-style graphs, classic orchestrator–worker patterns, and hand-written multi-agent harnesses. No product pitch. Just the model of the system.

Start With the Only Distinction That Matters

Before product names, hold one axis in your head:

Who holds the plan for what happens next?

  • If code (or a fixed graph) decides the next step, you are in workflow territory.
  • If the model decides the next step turn by turn, you are in agent territory.
  • If the model writes code that then decides, you are in the interesting middle that Claude Code calls a dynamic workflow.

That third case is why the marketing is confusing. The generation of the plan is dynamic and model-driven. The execution of the plan is deterministic-ish script execution over many workers. You get model creativity up front and code-shaped control during the run.

Anthropic’s 2024 essay Building Effective Agents still frames the industry baseline clearly: workflows orchestrate LLMs and tools through predefined code paths; agents let the LLM direct its own process and tool use. Prompt chaining, routing, parallelization (sectioning and voting), orchestrator–workers, and evaluator–optimizer are all named workflow patterns in that lineage. Claude Code’s dynamic workflows are a productized way to generate and run a large parallel variant of those patterns — especially orchestrator–workers plus voting / adversarial review — without you hand-writing the harness every time.

What Claude Code Dynamic Workflows Actually Are

According to Claude Code’s own docs, a dynamic workflow is:

  • a JavaScript script
  • that orchestrates many subagents
  • which Claude writes for the task you describe
  • and which a runtime executes in the background while your main session stays usable

Requirements (as documented at the time of writing): Claude Code v2.1.154+, paid plans (with Pro needing an explicit enable in /config), and availability through Anthropic API access as well as Bedrock / Google’s agent platform / Microsoft Foundry depending on how you run Claude Code.

You reach for Claude Code dynamic workflows when a job needs more agents than one conversation can comfortably coordinate, or when you want the orchestration itself to be something you can read, save, and rerun. The docs’ own examples are the right scale of ambition:

  • codebase-wide bug or auth sweeps
  • large migrations (hundreds of files)
  • research that must cross-check sources against each other
  • high-stakes plans drafted from several independent angles before you commit

The important architectural claim: the script holds the loop, the branching, and the intermediate results. The main Claude context is supposed to hold the final answer, not every partial tool trace from fifty workers. That is the opposite of “one giant chat where the lead model remembers everything.”

The Claude Code Menu: Four Primitives, One Axis

Claude Code already had multi-step machinery before dynamic workflows. The docs compare them by asking who holds the plan:

Primitive What it is Who decides what runs next Where intermediate results live Scale (order of magnitude)
Subagents Workers the main Claude spawns Main Claude, turn by turn Main context window A few delegated tasks per turn
Skills Packaged instructions / procedures Claude follows Claude, guided by the skill Main context window Same order as subagents
Agent teams Lead + peer sessions with shared task list / messaging Lead (and peers), turn by turn Shared task list + messages A handful of long-running peers
Dynamic workflows A script the runtime executes The script Script variables Dozens to hundreds of agents per run

I read that table as three different coordination problems:

  1. Subagents — “do this side quest and bring me a summary.” Great for context hygiene on a small number of tasks. Bad as your only tool for a 400-file migration, because the lead still serializes the world through one brain.
  2. Agent teams — “a few specialists who can negotiate.” Useful when the work is collaborative and the plan must change mid-flight because peers disagree. Expensive and chatty if what you actually needed was embarrassingly parallel map-reduce.
  3. Dynamic workflows — “encode the campaign, fan out, reduce, maybe verify, then report.” Useful when the structure is more stable than the individual file or claim being processed.

Skills sit sideways to all three. A skill is reusable procedural knowledge — how we do migrations here, how we write ADRs, which checks must pass — not a concurrency primitive. A workflow or a team can use skills; a skill does not replace a fan-out runtime.

How You Trigger One (Conceptually)

You do not need to memorize every CLI flag to understand the shape. Claude Code documents three entry points:

  1. Bundled workflow commands — for example /deep-research, which fans out searches, cross-checks sources, votes on claims, and returns a cited report with weak claims filtered.
  2. Ask for a workflow in the prompt — natural language (“use a workflow”) or the ultracode keyword (older builds used a literal workflow trigger). Claude writes a script for that task.
  3. Session policy: /effort ultracode — higher reasoning effort plus “plan a workflow for substantive tasks” as the default behaviour for the session. Powerful, and deliberately more expensive. It is session-scoped, not a permanent personality transplant.

Before a run, you typically get a plan / phase list to approve (depending on permission mode). During a run, the main session stays interactive; you inspect progress through a workflow UI rather than drowning in fifty interleaved tool transcripts. After a good run, you can save the script as a reusable command — which is the quiet killer feature. Dynamic generation becomes a candidate for a semi-static asset.

That last point is where practitioners split. Some people want Claude to invent a new orchestration every time. Others (myself included, on anything that touches production code) want the second run to be a git-reviewed harness with known phases, known concurrency, and known verification. Claude Code’s “save the workflow” path is the bridge between those camps.

What the Script Is Doing Under the Hood

You can understand the script without seeing every API surface. Conceptually it is ordinary control flow plus agent calls:

  • Phases — understand → change → verify, or search → fetch → vote → synthesize
  • Fan-out — spawn many subagents with isolated context, each holding one slice (one file, one claim, one module)
  • Synchronization — wait for a wave to finish before the next phase
  • Reduction — merge findings, dedupe, score, or pick a winner
  • Adversarial checks — independent agents try to refute claims or break a plan before you see it
  • Terminal report — one artifact back to the parent session

Workers inside a classic dynamic workflow are often isolated on purpose: they do not peer-chat the way an agent team does. That is a feature when you want independent judgments (voting, security review). It is a limitation when the task requires live negotiation (“your migration step conflicts with mine; let’s redesign the interface”).

This is also why dynamic workflows pair so well with an objective oracle — a test suite, a typechecker, a linter policy, a golden file. If the only judge of success is another model saying “looks good,” you have scaled vibes. If the script can loop on dotnet test or npm test until green, you have scaled engineering.

Failure Modes Dynamic Workflows Are Trying to Fix

Community write-ups and Anthropic’s own framing keep returning to the same single-agent pathologies:

Failure mode What it looks like in one chat How a workflow changes the game
Agentic laziness Stops after 35 of 50 files and declares victory The loop lives in code; completion is structural, not vibes
Self-preferential bias The same model grades its own work too kindly Separate verifier agents; adversarial review phases
Goal drift Long compacted context forgets the original objective Objective and phase structure sit in the script, not only in memory
Context congestion Every tool result lands in one window Workers keep private context; parent sees reductions

None of this is free. Parallel agents multiply tokens. A workflow that “almost works” can be a very expensive almost. Treat fan-out as a capacity planning problem, not a personality setting.

Equivalents Outside Claude Code

If you never touch Claude Code, you still need the same ideas. Here is how I map them.

1. Anthropic’s classical workflow patterns

From Building Effective Agents:

  • Prompt chaining — fixed pipeline of LLM steps with optional gates
  • Routing — classify, then specialized prompts
  • Parallelization — sectioning (split work) and voting (multiple attempts)
  • Orchestrator–workers — a lead decomposes, workers execute, lead synthesizes
  • Evaluator–optimizer — generate, critique, refine in a loop

A Claude dynamic workflow is often “orchestrator–workers + parallelization + optional evaluator,” except the orchestrator first emits a script instead of staying in the chat loop for every assignment.

2. LangGraph and friends

In LangGraph-style systems, the graph topology is usually defined up front (nodes and edges). Runtime dynamism comes from conditional edges, loops, and APIs that fan out work — not from the model inventing a brand-new graph every time (unless you build that yourself). So:

  • A static graph with conditional routing ≈ classical workflow with some agent-like choices inside nodes
  • A ReAct node loop ≈ agent behaviour inside a workflow chassis
  • A map-reduce subgraph over files ≈ what dynamic workflows do well in Claude Code

If your org already standardizes on LangGraph, CrewAI, Temporal + LLM activities, or plain .NET / Node orchestration, you do not need Claude’s product feature to implement the concept. You need clear phases, isolated workers, a reduce step, and an objective stop condition.

3. Hand-written multi-agent harnesses (including “deterministic workflows”)

A growing camp argues the opposite of fully dynamic generation: commit the orchestration script yourself, review it like any other code, and let models only fill worker roles. That is not anti-Claude; it is the same end-state as “save the workflow,” just with a human (or a PR) as the author of record.

I use this rule of thumb:

  • Explore with generation — let the tool propose a workflow when the shape of the work is still fuzzy.
  • Industrialize with a committed harness — once you know the phases, encode them, test them, and stop paying the “rediscover the plan” tax.

4. Agent SDKs vs workspace orchestration

There is a second vocabulary split worth naming:

  • Embedded agents (Agent SDK style) — agents you ship inside a product for end users
  • Workspace agents (Claude Code style) — agents that do your engineering / research work in a coding environment

Dynamic workflows in Claude Code are primarily a workspace-orchestration feature. The Agent SDK path is how you build similar loops into software you distribute. Same patterns, different runtime and trust boundary. If you also run tool servers through something like MCP, the gateway / policy questions I wrote about in the enterprise MCP gateway post sit under both.

A Simple Decision Guide

When I choose a primitive, I ask four questions:

  1. How parallel is the work really? Embarrassingly parallel file/claim work → workflow or map-reduce graph. Tightly coupled design negotiation → agent team or a senior human.
  2. Does the plan need to change mid-run based on peer debate? Yes → team / agent loop. No → scripted workflow.
  3. Do I have an objective checker? Tests, compilers, schemas, policy engines. If no, keep the blast radius small; verification theatre scales poorly.
  4. Will I run this again next month? If yes, invest in a saved or hand-written harness. If no, a one-shot generated workflow is fine.

And the anti-patterns:

  • Using ultracode-style always-on workflows for “rename this variable.”
  • Spawning fifty agents to rubber-stamp each other with no external oracle.
  • Calling everything an agent when you actually built a prompt chain.
  • Assuming “more agents” means “more truth.” Sometimes it only means more correlated mistakes at higher cost.

How This Fits a Broader AI Architecture

Dynamic workflows are an execution strategy, not a complete platform:

  • Models provide judgment and generation.
  • Tools / MCP servers provide capabilities (see also transport and gateway choices in production).
  • Skills / runbooks provide institutional procedure.
  • Orchestration (workflow scripts, graphs, teams) provides control flow and scale.
  • Evaluation and telemetry tell you whether any of it was worth the bill.

If you only invest in orchestration fashion and skip tools, policy, and evals, you get a very fast way to be wrong in parallel. If you invest in solid tools and a boring orchestrator, you often beat a flashy multi-agent demo.

Frequently Asked Questions

What is a dynamic workflow in Claude Code?

A JavaScript orchestration script—usually written by Claude for your task—that a runtime executes to coordinate many subagents in phases, keeping intermediate state in the script rather than in one giant chat context.

How is that different from an “AI agent”?

In classical Anthropic language, an agent decides each next action in a loop. A dynamic workflow uses agents (subagents) as workers, but the next phase and fan-out are driven by the script. Generation of the script can be agentic; execution is workflow-shaped.

Dynamic workflow vs agent team — which should I use?

Use a dynamic workflow for large, decomposable, mostly independent units of work with a stable phase structure. Use an agent team when a small number of peers must negotiate and revise a shared plan while they work.

Dynamic workflow vs subagent?

A subagent is one delegated worker under a turn-by-turn lead. A dynamic workflow is a programmed campaign that may spawn dozens or hundreds of such workers with explicit phases and reductions.

Is LangGraph a dynamic workflow?

Not automatically. Most LangGraph apps are predefined graphs with runtime path choices. That implements the same family of ideas (especially orchestrator–workers and loops), but the plan is usually authored by developers, not regenerated as a fresh JS file each run—unless you build that generation layer yourself.

Why do people say dynamic workflows burn tokens?

Because parallelism multiplies model calls. A research or migration workflow can be excellent value per engineering hour and still be a bad value per dollar if you point it at trivial tasks or leave ultracode on for everything.

Should the orchestration script be saved?

If you will repeat the job, yes. Treat a saved workflow like any automation: review it, constrain its tools, and prefer objective stop conditions. One-off exploration can stay ephemeral.

Do I need dynamic workflows to build multi-agent systems in production?

No. You need clear decomposition, isolation, reduction, authz around tools, and evaluation. Claude Code workflows are one high-leverage way to get that for workspace tasks. Product agents more often use SDKs, queues, and your own orchestrator.

Closing

Strip away the product names and you are left with an old distributed-systems problem: where does control flow live, and how do workers share results without drowning the coordinator?

Claude Code’s answer with dynamic workflows is crisp. Let the model propose a campaign. Put the campaign in a script. Run many isolated workers. Reduce. Optionally attack your own answers. Bring one report home. Compared with a single heroic chat session, that is a more honest machine for migrations, audits, and cross-checked research. Compared with a carefully committed harness, it is a faster way to discover what the harness should have been.

Learn the concept once, and every vendor checklist starts to rhyme — Claude workflows, LangGraph maps, orchestrator–worker services, even the multi-agent setups we wire through MCP tool servers. The fashionable noun will change again. The question “who holds the plan?” will not.

How I Design an Enterprise MCP Gateway in .NET

Once you have more than one MCP server, the question stops being “how do I expose a tool?” and becomes “how do I expose many tools without turning every AI client into a spaghetti of credentials, transports, and half-documented endpoints?” That is the moment people start talking about an MCP gateway.

I have already written about what MCP servers are and how I choose between stdio, Streamable HTTP, and SSE. This post sits one layer above those pieces. It is the architecture I reach for when local demos are fine, but a real team needs a single place to attach Claude Code, Cursor, or an internal agent host — with auth, allowlists, and logs that an enterprise security review will not immediately reject.

I am not selling a product here. This is the pattern I would sketch on a whiteboard for a .NET shop that already runs APIs on Azure and does not want every agent process spawning its own copy of every tool server.

What an MCP Gateway Actually Is

In the wild, “MCP gateway” gets used for three slightly different things:

  1. A reverse proxy for JSON-RPC — one public endpoint, many backend MCP servers.
  2. A transport adapter — wrap stdio servers so remote clients can reach them over Streamable HTTP.
  3. A policy plane — authentication, per-tool allowlists, rate limits, audit trails, and catalog aggregation.

A useful enterprise gateway is all three. If you only reverse-proxy without policy, you have moved the mess behind a nicer hostname. If you only do policy without transport bridging, half your existing stdio servers stay stuck on laptops.

The mental model I use is deliberately boring:

  • Northbound: one MCP endpoint that clients understand (preferably Streamable HTTP).
  • Southbound: many servers — some remote Streamable HTTP, some legacy SSE, some local stdio wrapped by the gateway process or a sidecar.
  • Control plane: registry of servers/tools, authn/authz, observability.

That is closer to an API gateway plus a service registry than it is to “another chat plugin.”

Why Teams Outgrow Point-to-Point MCP

Point-to-point works for a single developer. It falls over in familiar ways once more people join:

  • Every client config file lists five servers, five URLs, and five secret storage stories.
  • stdio servers cannot be shared cleanly across a team or a CI runner.
  • Tool names collide (search, query, run) with no namespace strategy.
  • Security asks who can call execute_sql and you have no answer beyond “the laptop that has the config.”
  • You cannot rotate a backend without editing every client.

None of that means MCP is wrong. It means the integration boundary needs a facade, the same way we stopped giving every browser a direct connection to every microservice.

Reference Architecture I Would Use in .NET

Here is the shape I would implement first, then harden.

Layer Responsibility .NET-friendly choice
Edge TLS, WAF, private networking Azure Front Door / App Gateway, or internal only via VNet
Gateway host MCP session, routing, policy ASP.NET Core + official MCP C# SDK transports
Identity Who is calling Entra ID (OAuth 2.1 / OIDC), workload identity for services
Registry Server catalog, tool metadata, versions Config + SQL/Cosmos; eventually a small admin API
Backends Actual tools Existing MCP servers, internal APIs wrapped as tools
Telemetry Traces, tool audit, failures OpenTelemetry → App Insights

Two design choices I treat as non-negotiable for anything beyond a lab:

  1. Northbound transport is Streamable HTTP (stateful only if you truly need sessions; stateless when you can get away with it). Clients that only speak stdio can use a thin local bridge, but the shared service is HTTP.
  2. The gateway never becomes the place business logic lives. It routes, authorizes, and observes. Domain rules stay in the backend servers or existing APIs.

Core Responsibilities, in Practice

1. Catalog aggregation

Clients should see a merged tool list. Behind the scenes you will:

  • Connect to each registered backend
  • Pull tools/list (and prompts/resources if you expose them)
  • Prefix or namespace tool names to avoid collisions — for example billing.refund vs bare refund
  • Refresh on a schedule or on admin signal when a backend deploys

I prefer explicit namespaces over hoping two teams never pick the same verb.

2. Request routing

When a client calls a tool, the gateway:

  1. Authenticates the caller
  2. Checks allowlist / role policy for that tool
  3. Maps the public tool name to a backend + original tool name
  4. Forwards the JSON-RPC call with a correlation id
  5. Returns the result (or a sanitized error)

Keep the public schema stable even if a backend renames an internal tool. That stability is half the point of the facade.

3. Transport bridging

stdio is still the fastest way to build a tool server on a workstation. In production I wrap those processes deliberately — supervisor restarts them, the gateway owns their lifetime, and they are not reachable except through the gateway. Remote backends should already speak Streamable HTTP. Legacy SSE gets a compatibility adapter with a sunset date, not a forever home. I covered the trade-offs in the transport comparison.

4. Auth and tool policy

MCP’s authorization story has matured around HTTP transports and OAuth-style flows. In an enterprise .NET environment I still design policy the way I would for any sensitive API:

  • Service-to-service: managed identity / client credentials where appropriate
  • User-delegated agents: Entra ID tokens with clear audience and scopes
  • Tool-level authorization: not just “is logged in,” but “may call payments.capture
  • Optional approval workflows for high-risk tools (human in the loop) outside the protocol if needed

If everything is allowed once you pass the front door, you built a reverse proxy with better marketing.

A Minimal .NET Sketch

The following is intentionally incomplete. It shows the seams I care about — registry, policy, and forward — rather than a full production host. Use the official MCP C# SDK types for the real wire protocol; treat this as architecture-shaped pseudocode that compiles in spirit.

public sealed record McpBackend(
    string Id,
    string PublicNamespace,
    Uri? HttpEndpoint,
    BackendKind Kind);

public enum BackendKind { StreamableHttp, StdioSupervised }

public interface IToolCatalog
{
    Task RefreshAsync(CancellationToken ct);
    bool TryResolve(string publicToolName, out ResolvedTool tool);
}

public sealed record ResolvedTool(
    string BackendId,
    string UpstreamToolName,
    IReadOnlyList<string> RequiredRoles);

public sealed class GatewayToolDispatcher
{
    private readonly IToolCatalog _catalog;
    private readonly IAuthorizationService _authz;
    private readonly IBackendClientFactory _clients;
    private readonly ILogger<GatewayToolDispatcher> _log;

    public GatewayToolDispatcher(
        IToolCatalog catalog,
        IAuthorizationService authz,
        IBackendClientFactory clients,
        ILogger<GatewayToolDispatcher> log)
    {
        _catalog = catalog;
        _authz = authz;
        _clients = clients;
        _log = log;
    }

    public async Task<ToolResult> InvokeAsync(
        ClaimsPrincipal caller,
        string publicToolName,
        BinaryData arguments,
        string correlationId,
        CancellationToken ct)
    {
        if (!_catalog.TryResolve(publicToolName, out var tool))
            return ToolResult.Error("tool_not_found", $"Unknown tool '{publicToolName}'.");

        var decision = await _authz.AuthorizeAsync(caller, tool, ct);
        if (!decision.Succeeded)
        {
            _log.LogWarning(
                "Denied {Tool} for {Subject} corr={CorrelationId}",
                publicToolName, caller.Identity?.Name, correlationId);
            return ToolResult.Error("forbidden", "Not allowed to invoke this tool.");
        }

        var client = _clients.Get(tool.BackendId);
        using var scope = _log.BeginScope(new Dictionary<string, object>
        {
            ["tool"] = publicToolName,
            ["backend"] = tool.BackendId,
            ["correlationId"] = correlationId
        });

        // Forward as upstream tools/call with mapped name.
        return await client.CallToolAsync(tool.UpstreamToolName, arguments, correlationId, ct);
    }
}

A few implementation notes from real systems work:

  • Correlation ids belong on every hop. When an agent misbehaves at 2am, you will want one id that ties client request → gateway → backend logs.
  • Argument schemas should be validated at the edge. Do not assume backends are hostile-input-proof.
  • Timeouts and cancellation matter more than people expect; tool calls are often long and LLM clients retry enthusiastically.
  • Idempotency is not free. If a client retries create_invoice, decide whether the gateway or the backend owns deduplication keys.

Configuration Beats Cleverness

Early on I keep the registry in configuration plus a small database table, not a distributed discovery fantasy.

{
  "McpGateway": {
    "Backends": [
      {
        "Id": "billing",
        "PublicNamespace": "billing",
        "Kind": "StreamableHttp",
        "HttpEndpoint": "https://billing-mcp.internal/"
      },
      {
        "Id": "ops-scripts",
        "PublicNamespace": "ops",
        "Kind": "StdioSupervised",
        "Command": "dotnet",
        "Args": ["OpsMcp.dll"]
      }
    ],
    "DefaultDeny": true
  }
}

DefaultDeny is deliberate. New tools stay dark until someone maps roles. That is mildly annoying in week one and deeply comforting in month six.

Security Review Talking Points

If you present this design inside a bank, telco, or large government supplier, expect these questions. Having answers ready is part of the architecture.

Question Answer shape I use
Where do secrets live? Backend credentials in Key Vault / managed identity; never in client MCP configs.
Can the model see admin tools? Only if the caller’s token maps to a role that includes them; catalog is filtered per principal.
What is logged? Tool name, subject, latency, success/failure, correlation id — not raw secrets or full PII payloads.
Blast radius if the gateway is compromised? Network isolation to backends, short-lived credentials, per-backend least privilege.
How do we revoke access? IdP group removal + gateway policy cache TTL measured in minutes, not days.

I also keep a hard list of tools that must never be exposed to a general-purpose coding agent — production data deletion, unrestricted shell, wire-transfer equivalents. Those either stay off the gateway or require a separate break-glass path.

Where Gateways Go Wrong

Patterns I have seen (and occasionally caused):

  • God gateway. Business rules, prompt templates, and five product teams’ special cases all land in one repo. Split backends early.
  • Transparent proxy with no schema discipline. Tool names thrash every sprint; clients break; people blame “AI.”
  • stdio farm without supervision. Orphan processes, leaked file handles, mysterious port conflicts.
  • Logging full tool arguments “just for debug.” That is how payroll numbers end up in a log analytics workspace with the wrong retention policy.
  • Ignoring multi-tenancy. If two business units share a gateway, tenant isolation is a first-class design input, not a later flag.

How This Fits the Rest of an AI Platform

An MCP gateway is not a model router and not an agent framework. Keep those boundaries clean:

  • Model gateway / LLM proxy — tokens, model selection, spend caps.
  • Agent runtime — planning, memory, multi-step loops.
  • MCP gateway — tools as capabilities with enterprise controls.

When those three collapse into one binary, upgrades get political. When they stay separate, you can swap an agent host without re-litigating tool security.

If your world also includes operational systems — and mine often does, including EV charging platforms speaking OCPP to a CSMS — the same facade thinking applies. Agents should not get a raw socket to production infrastructure. They get mediated tools with audit trails. The technology changes; the integration discipline does not.

Frequently Asked Questions

What is an MCP gateway?

An MCP gateway is a mediating service between AI clients and one or more MCP servers. It aggregates tool catalogs, enforces authentication and authorization, bridges transports such as stdio and Streamable HTTP, and provides a single stable endpoint for clients.

Do I need an MCP gateway for local development?

Usually no. stdio servers wired directly into Claude Desktop or an IDE are fine for personal workflows. Introduce a gateway when multiple users, multiple servers, or shared credentials enter the picture.

Should the gateway use stdio or Streamable HTTP northbound?

For anything shared, prefer Streamable HTTP northbound. Keep stdio for local clients or as a southbound implementation detail behind the gateway.

Is an MCP gateway the same as an API gateway?

Related idea, different protocol concerns. You still want TLS termination, identity, and routing, but you must handle MCP’s JSON-RPC methods, tool catalogs, and sometimes long-lived sessions — not only REST paths.

How do I prevent tool name collisions?

Namespace by backend or domain (billing.refund, ops.restart_worker) and treat public names as a stable contract owned by the gateway team.

Can I put every internal API behind MCP?

You can, but you should not. Expose small, intention-revealing tools with clear side effects. A 200-endpoint OpenAPI surface dumped into tool form overwhelms models and frightens security reviewers.

Closing

The useful question is not “do we need an MCP gateway?” It is “where do we want the control point for agent tools to live?” If the answer is “in twelve desktop config files,” you already know the next outage’s shape.

Start with one HTTP endpoint, one backend, real identity, and default-deny policy. Add namespaces, telemetry, and stdio supervision when the first design proves itself. The protocol will keep evolving; a clear facade still ages better than a pile of special cases.

If you want the foundations under this post, read What Are MCP Servers? and MCP Transport Types Explained next — or again, with the gateway lens in mind.

MCP Transport Types Explained: stdio vs Streamable HTTP vs SSE in C#

Nobody tells you this when you start building MCP servers, but picking the wrong MCP transport is the kind of mistake that doesn’t hurt immediately. It hurts three months later when you’re trying to deploy to Azure and realise your architecture assumes a single process on a single machine. I’ve been there. This post is what I wish I’d read before that happened.

The MCP C# SDK gives you four transport options: stdio, Streamable HTTP, SSE, and in-memory. They all carry the same JSON-RPC 2.0 messages between your AI client and your server — the difference is in where your server lives, how connections are managed, and what happens under production load. I’ll walk through each one honestly, with working C# code and the stuff the official docs gloss over.

If you haven’t read my post on what MCP servers actually are, start there first — this one assumes you already know the basics.

What Is an MCP Transport, Exactly?

The simplest way I can put it: the MCP transport is the pipe that carries messages between the AI model and your server. The protocol itself — tool calls, capability manifests, responses — is identical regardless of which transport you use. What changes is the physical channel those messages travel through.

Think of it like this. The conversation between you and a colleague is the same whether you’re in the same room, on a video call, or texting. The transport just determines who can initiate, how fast messages arrive, and what happens when the connection drops.

The Four Transports at a Glance

Before I go into detail on each one, here’s the map. I’ll refer back to this table throughout.

Transport Direction Sessions Scales horizontally? Best for
stdio Bidirectional Implicit — one per process N/A Local tools, Claude Desktop, IDE integrations
Streamable HTTP (stateless) Request-response None ✅ No constraints Production APIs, Azure Container Apps
Streamable HTTP (stateful) Bidirectional Mcp-Session-Id header ⚠️ Needs sticky sessions Long-running agents, server push notifications
SSE (legacy) Server→client stream + POST Query string session ID ⚠️ Needs sticky sessions Legacy client compatibility only
In-memory Bidirectional Implicit — one per pipe N/A Unit tests, same-process embedding

1. stdio — Start Here, Always

stdio is the one that surprised me the most when I first encountered MCP. The client literally launches your server as a child process and the two talk through stdin and stdout. No HTTP, no ports, no certificates. Your server is just a program reading from one pipe and writing to another.

This is how Claude Desktop connects to local MCP servers. You add your server to claude_desktop_config.json, Claude spawns the process, and from that point on the two are talking JSON-RPC over standard I/O. It’s almost offensively simple for something that enables quite sophisticated agent behaviour.

What I like about it

  • It just works, everywhere. No networking, no firewall rules, no TLS ceremony. If you can run the binary, you have a working MCP server. I’ve got stdio servers running in environments where I’m not allowed to open any network ports.
  • Bidirectional by default. The server can push notifications back to the client at any time — stdin/stdout flow control gives you natural backpressure with zero configuration.
  • Clean isolation. Each client connection gets its own process. If the server crashes, the client knows immediately and only that session is affected. Nothing shared, nothing leaked.
  • Easiest to debug. You can literally run the server in a terminal and type JSON at it. I’ve done this more times than I’d like to admit.

What to watch out for

  • Local only. The server has to live on the same machine as the client. This rules it out for any cloud or API-style deployment.
  • One client per process. Not a problem for dev tools, but if you’re imagining dozens of agents sharing one server instance — that’s not stdio’s job.
  • Secret leakage is a real risk. This one caught me off guard. By default, your child process inherits every environment variable from the parent — which means GITHUB_TOKEN, OPENAI_API_KEY, AWS_SECRET_ACCESS_KEY, all of it flows straight to your server (or a third-party server you’re connecting to). The fix is one option, but you have to know to set it.

Server — the code is three lines

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithTools<MyTools>();

// stdout belongs to the protocol — logs must go to stderr
builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});

await builder.Build().RunAsync();

Client

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Command = "dotnet",
    Arguments = ["run", "--project", "MyMcpServer"],
    ShutdownTimeout = TimeSpan.FromSeconds(10)
});

await using var client = await McpClient.CreateAsync(transport);

The environment variable thing — actually fix this

Don’t skip this, especially if you’re connecting to any third-party MCP server. The safe pattern is to start from the SDK’s curated allowlist and add only what your specific server needs:

// GetDefaultEnvironmentVariables() gives you PATH, HOME, and standard
// system dirs — nothing sensitive
var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
env["MY_SERVER_API_KEY"] = apiKey; // add only what's needed

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Command = "my-mcp-server",
    InheritEnvironmentVariables = false, // <-- this is the important bit
    EnvironmentVariables = env,
});

If you’re writing the server yourself and you know exactly what it needs, this feels like overkill. If you’re connecting to someone else’s server — treat it like any other third-party code and don’t hand it your credentials by default.

2. Streamable HTTP — What You Want in Production

Once your MCP server needs to live somewhere other than the developer’s laptop, Streamable HTTP is the answer. It’s the transport I reach for on every Azure deployment, and the one the SDK team clearly considers the future of the protocol.

Here’s how it works: the client sends an HTTP POST. The server holds that POST response body open as an SSE stream and writes the JSON-RPC response through it — along with any intermediate messages like progress updates. So you get the reliability and auth ecosystem of HTTP, with real-time streaming baked in. Clever.

It comes in two flavours and the choice matters a lot for your deployment architecture.

Stateless mode — what I use by default

Every request is independent. No session tracking. No in-memory state between calls. This is gloriously simple to operate — deploy three instances behind Azure Front Door and every request can land on any instance. No sticky session configuration, no session replication, no “which node has this user’s state” debugging at 2am.

The trade-off: the server can’t send unsolicited messages to the client. If your tools are pure request-response — client asks, server answers — stateless is perfect and I’d argue it’s the right default for most tool-use scenarios.

Stateful mode — when you need server push

If your server needs to push notifications mid-conversation — progress updates on a long-running job, real-time alerts, streaming intermediate results — you need stateful mode. The server issues an Mcp-Session-Id header after the first request and tracks per-session state in memory. Clients can also open long-lived GET requests to receive unsolicited notifications.

The cost is operational: you need session affinity at your load balancer. On Azure Container Apps this means pinning the session to a specific replica. Not complicated, but it’s a constraint you need to plan for.

Server code

dotnet add package ModelContextProtocol.AspNetCore

Stateless (the default I recommend):

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
    })
    .WithTools<MyTools>();

var app = builder.Build();
app.MapMcp(); // registers POST /mcp and GET /mcp
app.Run();

Stateful (when you need server push):

builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = false;
    })
    .WithTools<MyTools>();

Client

// AutoDetect tries Streamable HTTP first, falls back to SSE if needed
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("https://my-mcp-server.example.com/mcp")
    // TransportMode defaults to AutoDetect
});

await using var client = await McpClient.CreateAsync(transport);

Session resumption — more useful than it sounds

In stateful mode, if the connection drops mid-conversation, you don’t have to start from scratch. Store the session ID and server info after connecting, then resume:

// Save these after first connection
string savedSessionId = client.SessionId;
McpServerCapabilities savedCaps = client.ServerCapabilities;
McpImplementation savedServerInfo = client.ServerInfo;

// On reconnect
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("https://my-mcp-server.example.com/mcp"),
    KnownSessionId = savedSessionId
});

await using var client = await McpClient.ResumeSessionAsync(transport, new ResumeClientSessionOptions
{
    ServerCapabilities = savedCaps,
    ServerInfo = savedServerInfo
});

For long-running agent workflows — the kind where the model is orchestrating a multi-step task over several minutes — this is the difference between a graceful reconnect and a broken session that kills the job.

If you have browser clients (CORS)

Most MCP clients aren’t browsers, but if yours is, you’ll need CORS. One thing worth emphasising: CORS is not a substitute for host name validation — you need both. The CORS config tells browsers which origins are allowed; host name validation protects against DNS rebinding attacks from non-browser clients.

var allowedOrigins = builder.Configuration
    .GetSection("Mcp:AllowedOrigins")
    .Get<string[]>() ?? ["http://localhost:5173"];

builder.Services.AddCors(options =>
{
    options.AddPolicy("McpBrowserClient", policy =>
    {
        policy.WithOrigins(allowedOrigins)
            .WithMethods("POST", "GET", "DELETE")
            .WithHeaders("Content-Type", "Authorization",
                         "MCP-Protocol-Version", "Mcp-Session-Id")
            .WithExposedHeaders("Mcp-Session-Id");
    });
});

app.UseCors();
app.MapMcp("/mcp").RequireCors("McpBrowserClient");

3. SSE — Use It Only If You Have To

I’ll be straight with you: I wouldn’t choose SSE for anything new. The SDK marks EnableLegacySse as obsolete with diagnostic MCP9004, and that label exists for a reason.

SSE splits communication across two endpoints: your server streams to the client over a /sse connection, and the client sends messages back via HTTP POST to a /message endpoint. It was the original remote transport before Streamable HTTP arrived, and it has a structural flaw that’s hard to work around: the POST endpoint returns HTTP 202 before your handler even runs, which means there’s no backpressure. Under load, requests pile up and you can’t signal to the client that the server is overwhelmed.

The only reason to use SSE today is compatibility. Some older MCP clients — early Claude Desktop builds, some third-party tools — only speak SSE. If you need to support them alongside newer clients, run SSE alongside Streamable HTTP and let clients self-select.

Client

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("https://my-mcp-server.example.com/sse"),
    TransportMode = HttpTransportMode.Sse,
    MaxReconnectionAttempts = 5,
    DefaultReconnectionInterval = TimeSpan.FromSeconds(1)
});

Server — note the pragma

builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = false; // SSE requires stateful mode

#pragma warning disable MCP9004
        options.EnableLegacySse = true;
#pragma warning restore MCP9004
    })
    .WithTools<MyTools>();

The fact that you need a pragma suppression to enable it should tell you everything about the SDK team’s intentions here.

4. In-Memory — Your Testing Best Friend

This one doesn’t get talked about enough. The in-memory transport connects a client and server using System.IO.Pipelines inside the same process — no network, no serialisation overhead, no infrastructure. For unit and integration tests it’s genuinely great.

What I like about it: your tests exercise the real MCP protocol, not mocks. Tool registration, schema generation, capability negotiation — all of it runs for real, just without a network between the two sides. I caught a schema mismatch bug in a tool’s [Description] attribute using an in-memory test that I’d completely missed in manual testing.

using System.IO.Pipelines;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

Pipe clientToServer = new(), serverToClient = new();

await using var server = McpServer.Create(
    new StreamServerTransport(
        clientToServer.Reader.AsStream(),
        serverToClient.Writer.AsStream()),
    new McpServerOptions
    {
        ToolCollection =
        [
            McpServerTool.Create(
                (string message) => $"Echo: {message}",
                new() { Name = "echo" })
        ]
    });

_ = server.RunAsync();

await using var client = await McpClient.CreateAsync(
    new StreamClientTransport(
        clientToServer.Writer.AsStream(),
        serverToClient.Reader.AsStream()));

var tools = await client.ListToolsAsync();
var echo = tools.First(t => t.Name == "echo");
Console.WriteLine(await echo.InvokeAsync(new() { ["arg"] = "Hello, MCP!" }));
// Output: Echo: Hello, MCP!

One limitation worth knowing: in-memory tests won’t catch network-specific failure modes — timeouts, dropped connections, serialisation edge cases from real socket buffers. Use in-memory for protocol correctness tests, and add a handful of real HTTP integration tests for your production transport paths.

Which MCP Transport Should You Use?

Here’s the decision in plain terms. I’ve seen people overthink this — pick the one that fits your deployment and move on.

Your situation Use this
Connecting to Claude Desktop, VS Code, or any local AI client stdio
Remote server on Azure / AWS, tools are pure request-response Streamable HTTP — stateless
Remote server, needs server push or long-running job tracking Streamable HTTP — stateful
Supporting a legacy client that only speaks SSE SSE (but plan to migrate)
Writing unit or integration tests In-memory
Not sure — want the client to handle it automatically HttpTransportMode.AutoDetect on the client

One More Thing: Enterprise SSO with the ID-JAG Flow

If you’re deploying an MCP server inside a corporate environment with Okta or Entra ID, you’ll likely hit the question of how agents authenticate without requiring users to log in every time. The SDK has a built-in solution for this: the Identity Assertion Grant flow.

In short: it exchanges an OIDC ID token from your identity provider for an MCP access token via a two-step RFC-standard token exchange. Your agent stays authenticated, the access token is cached until expiry, and you call InvalidateCache() if you get a 401 and need to refresh.

using ModelContextProtocol.Authentication;

var provider = new IdentityAssertionGrantProvider(
    new IdentityAssertionGrantProviderOptions
    {
        ClientId = "mcp-client-id",
        IdpTokenEndpoint = "https://company.okta.com/oauth2/token",
        IdpClientId = "idp-client-id",
        // This callback retrieves the current user's ID token from your SSO client
        IdTokenCallback = (context, cancellationToken) =>
            mySsoClient.GetIdTokenAsync(cancellationToken)
    },
    new HttpClient());

var tokens = await provider.GetAccessTokenAsync(
    resourceUrl: new Uri("https://mcp-server.example.com"),
    authorizationServerUrl: new Uri("https://auth.mcp-server.example.com"),
    cancellationToken: ct);

// On 401: provider.InvalidateCache() forces a fresh exchange next call

I’ll cover enterprise auth in more depth in a dedicated post — there’s enough there to warrant its own treatment, especially for Azure-hosted scenarios with Entra ID.

Frequently Asked Questions

What is the default MCP transport in the C# SDK?

There isn’t one — you choose explicitly when configuring the server. The most common starting points are WithStdioServerTransport() for local tools and WithHttpTransport() from ModelContextProtocol.AspNetCore for anything deployed remotely.

Can I switch transports without rewriting my tools?

Yes, completely. The transport is configured in Program.cs; your [McpServerTool] methods don’t know or care which transport is active. I’ve migrated projects from stdio to Streamable HTTP in under 10 minutes — it’s genuinely just a config change.

What is the difference between stateless and stateful Streamable HTTP?

Stateless means each HTTP POST is a standalone request — no session, no shared state, infinitely scalable. Stateful means the server issues an Mcp-Session-Id and tracks per-session state in memory, which enables server-to-client push notifications and session resumption but requires sticky sessions at your load balancer.

Is SSE still supported in the MCP C# SDK?

Yes, but it’s marked obsolete (diagnostic MCP9004). The SDK team recommends Streamable HTTP for all new work. SSE is there for backwards compatibility with older clients and will likely be removed in a future major version.

What transport does Claude Desktop use?

stdio. Claude Desktop launches your server as a child process using the command defined in claude_desktop_config.json and communicates over stdin/stdout. This is why local MCP servers for Claude are so simple to set up — no networking involved at all.

Can I run stdio and Streamable HTTP on the same server?

Not in a single AddMcpServer() setup, but the practical pattern is to put your tool logic in a shared class library and have two thin host projects — one with stdio for local dev tooling, one with Streamable HTTP for production. The tool code is identical; only the entry point differs.


Transport decisions look simple on paper and get complicated fast when you’re deploying to Azure with sticky session requirements and corporate SSO in the way. If you’re navigating that and want a second opinion, find me on LinkedIn — I’m in Sydney and happy to talk it through.

This is part of my MCP server series: What Are MCP Servers? · Real-World MCP Case Study · Production MCP Deployment on Azure.

What Are MCP Servers? A Complete Guide for AI Engineers

If you’re building AI agents that need to interact with real systems — databases, APIs, internal tools — you’ve probably run into the same wall I did. I’m talking about the mess of hardcoded JSON schemas in system prompts, brittle API wrappers, and production incidents that happen when the LLM invents a parameter name that doesn’t exist. An MCP server (Model Context Protocol server) is the structured solution to that problem, and as an AI engineer working in Sydney, it’s now a core part of how I build agent systems.

Let me walk you through what MCP servers are, why they matter, and how to build one in C# using the official SDK — with real, working code.

The Problem an MCP Server Solves

When you build an AI agent that needs to do things — query a database, call an API, read a file — you have to teach the model what tools exist, what they accept, and what they return. The old approach was: write it into the system prompt and pray.

The problem is that system prompts don’t have a schema. If your tool expects a date in ISO 8601 format and the model sends "tomorrow", your app crashes. If you rename a parameter, you need to update prompts scattered across multiple deployments. There’s no contract between the AI and your code.

MCP fixes this by defining a standard protocol — a contract — between AI models (clients) and the external capabilities they need (servers). An MCP server advertises what tools it offers, what inputs each tool expects, and what it returns. The model calls tools through MCP the same way a browser calls APIs through HTTP: with a defined interface that both sides agree on in advance.

This isn’t just theoretical tidiness. It’s what makes AI agents maintainable in production.

How an MCP Server Works

Think of MCP like a USB-C standard for AI tools. Before USB-C, every device had its own charger. After, one cable works everywhere. MCP does the same thing for AI capabilities.

Here’s the flow when an AI agent uses an MCP tool:

  1. The agent (MCP client) connects to your MCP server at startup.
  2. The server sends a capabilities manifest — a list of tools it provides, with their names, descriptions, and input schemas.
  3. The agent’s host (Claude, ChatGPT, your own orchestrator) reads this manifest and makes the tools available to the model.
  4. When the model decides to call a tool, it sends a structured request through the client.
  5. Your server executes the tool and returns a structured response.
  6. The model reads the result and continues reasoning.

The beauty of step 2 is that the model now has a machine-readable description of your tools — not a paragraph of English in a system prompt, but a typed schema. Fewer hallucinations. Cleaner architecture. Tools you can version and test independently.

The Three Things an MCP Server Can Expose

MCP servers can expose three types of capabilities:

Tools — Functions the model can call. Think of these as your API endpoints. A tool takes typed inputs and returns a result. This is what most people think of when they hear “MCP server.”

Resources — Read-only data the model can access. Like files, database records, or live feeds. The model can browse available resources and read their contents.

Prompts — Reusable prompt templates the model can invoke. Useful for standardising how certain tasks are framed across your application.

For most enterprise use cases, you’ll mostly be building Tools. Resources matter when you want the model to explore data rather than query it directly. Prompts are useful in multi-agent workflows.

Architecture Overview

Before we touch code, here’s how the pieces connect. The diagram shows the three zones: the AI client (Claude Desktop or your app), the MCP server (our C# application), and the backend systems it talks to. Use the zoom controls inside the diagram to explore it in detail.

MCP Server architecture: AI client → JSON-RPC protocol → C# server → backend systems. Pan and zoom inside the diagram, or open full-screen in draw.io ↗

Building a Real MCP Server in C#

Enough theory. Let me show you how this looks in code using the official MCP C# SDK (ModelContextProtocol), which Microsoft co-maintains with Anthropic. This is production-ready code — not a toy example.

Step 1: Install the packages

dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

This gives you the full MCP server runtime with the .NET generic host, dependency injection, and logging baked in.

Step 2: Project structure

MyMcpServer/
├── Program.cs
├── Tools/
│   └── ProductTools.cs
└── MyMcpServer.csproj

Step 3: Wire up the host in Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyMcpServer.Tools;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()   // Claude Desktop and most MCP clients use stdio
    .WithTools<ProductTools>();   // Register your tool class

// Log to stderr — stdout must stay clean for the MCP protocol
builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});

// Your tools get full DI — inject anything here
builder.Services.AddSingleton<IProductRepository, SqlProductRepository>();

await builder.Build().RunAsync();

Three things worth noting. First, AddMcpServer() wires up the entire MCP runtime — you write zero protocol handling. Second, WithStdioServerTransport() is for local Claude Desktop use; for Azure deployments you swap it for the ModelContextProtocol.AspNetCore HTTP transport. Third, your tool classes get full dependency injection — inject your database, HTTP clients, loggers, anything.

Step 4: Define your tools with attributes

using ModelContextProtocol;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace MyMcpServer.Tools;

[McpServerToolType]
public sealed class ProductTools
{
    private readonly IProductRepository _repo;

    public ProductTools(IProductRepository repo)
    {
        _repo = repo;
    }

    [McpServerTool, Description("Search for products by name or SKU. Returns matching products with current stock levels.")]
    public async Task<string> SearchProducts(
        [Description("The product name or SKU to search for.")] string query,
        [Description("Maximum number of results to return. Defaults to 10.")] int limit = 10)
    {
        var results = await _repo.SearchAsync(query, limit);

        if (!results.Any())
            return $"No products found matching '{query}'.";

        return string.Join("\n---\n", results.Select(p =>
            $"SKU: {p.Sku}\nName: {p.Name}\nStock: {p.StockLevel}\nPrice: {p.Price:C}"));
    }

    [McpServerTool, Description("Get detailed information about a specific product by its SKU.")]
    public async Task<string> GetProductDetails(
        [Description("The product SKU (e.g. PROD-12345).")] string sku)
    {
        var product = await _repo.GetBySkuAsync(sku);

        if (product is null)
            return $"No product found with SKU '{sku}'.";

        return $"""
            SKU: {product.Sku}
            Name: {product.Name}
            Description: {product.Description}
            Stock Level: {product.StockLevel} units
            Price: {product.Price:C}
            Category: {product.Category}
            Last Updated: {product.UpdatedAt:yyyy-MM-dd HH:mm} UTC
            """;
    }
}

The [McpServerTool] attribute registers the method as an MCP tool. The [Description] attributes on both the method and its parameters become the typed schema that the AI model reads — this replaces the fragile English paragraphs in your system prompt. If the description is precise, the model uses the tool correctly. If the parameter description specifies the expected format, the model sends the right format. Every time.

What the AI Model Actually Sees

When Claude or any MCP client connects to this server, it automatically receives a capabilities manifest generated from your C# code:

{
  "tools": [
    {
      "name": "SearchProducts",
      "description": "Search for products by name or SKU. Returns matching products with current stock levels.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "The product name or SKU to search for."
          },
          "limit": {
            "type": "integer",
            "description": "Maximum number of results to return. Defaults to 10.",
            "default": 10
          }
        },
        "required": ["query"]
      }
    }
  ]
}

The SDK generates this JSON schema from your C# method signatures automatically. No manual schema writing. No JSON maintenance. Rename a parameter in C#, the schema updates. Add a new tool method, it appears in the manifest. This is the developer experience that was missing before MCP.

Running It Locally with Claude Desktop

To test your MCP server with Claude Desktop, add it to your claude_desktop_config.json:

{
  "mcpServers": {
    "product-server": {
      "command": "dotnet",
      "args": ["run", "--project", "/path/to/MyMcpServer"]
    }
  }
}

Restart Claude Desktop, open a conversation, and ask: “What products do we have in the PROD-1 range?” Claude will automatically call SearchProducts, read the result, and answer from real data — not a hallucination.

That moment when you see the AI reach into your actual database and return a real answer is quietly satisfying in a way that’s hard to describe until you’ve experienced it.

When Should You Build an MCP Server?

Build one when:

  • You want the same capabilities available to multiple AI clients (Claude, Copilot, your internal tools) without duplicating integration code.
  • You’re building an AI agent that needs to interact with internal systems — databases, ERPs, CRMs — that aren’t publicly accessible.
  • You want to give your tools a proper versioned schema rather than relying on natural language in prompts.
  • You’re building a multi-agent system and need a reliable way for agents to share capabilities.

Don’t bother when you’re prototyping with a single model and a single capability. Use function calling directly. MCP pays off when you’re thinking about the long run — multiple clients, multiple tools, real maintainability.

The Bigger Picture

MCP is becoming the USB-C of AI tooling. Claude supports it natively. GitHub Copilot is adding support. OpenAI’s agent framework is converging on it. If you’re building AI systems that need to interact with the real world, this is the protocol worth learning now.

More importantly for us as engineers: it gives us a way to reason about AI tool integrations the same way we reason about APIs. Contract-first, typed, testable, versioned. That’s the kind of discipline that makes enterprise AI systems actually work in production.

I’ve been working with MCP servers on Azure-hosted agent systems, and the difference in maintainability compared to prompt-embedded tool descriptions is night and day. If you’re building anything beyond a demo, start here.


Working on an AI integration project in Sydney and want to talk through the architecture? Connect with me on LinkedIn — I’m always up for a conversation about what’s actually working in production.

Next up: How I Built an MCP Server for Enterprise Data — a Real-World Case Study, where I go deeper on authentication, error handling, and deploying to Azure Container Apps.

Unlocking C# 14: Practical New Features with Real-World Code Examples

C# 14 is more than just an incremental update—it’s a leap toward expressive, safe, and efficient coding. Whether you’re refactoring business logic or fine-tuning performance, these new features can make your codebase cleaner and easier to maintain. Let’s break down the C# 14 enhancements with side-by-side code samples.

1. field Keyword for Backing Fields

Previously, injecting logic into an auto-property required manual backing fields and verbose code:

<code><em>// Before (manual backing field)</em>
private string _name;
public string Name
{
    get => _name;
    set
    {
        if (value is null) throw new ArgumentNullException(nameof(value));
        _name = value;
    }
}

Now, you can add validation, transformation, or constraints—directly within the accessor using field:

<code><em>// After (C# 14 field keyword)</em>
public string Name
{
    get;
    set => field = value ?? throw new ArgumentNullException(nameof(value));
}

You can also use this for trimming, fallback, or range checks:

<code>public int Age
{
    get => field;
    set => field = (value >= 0) ? value : throw new ArgumentOutOfRangeException(nameof(value), "Age must be non-negative");
}

2. Null-Conditional Assignment

No more repetitive if-statements for null-checking assignments:

<code><em>// Before</em>
if (customer is not null)
{
    customer.Order = GetCurrentOrder();
}

<em>// After (C# 14)</em>
customer?.Order = GetCurrentOrder();

With compound assignments also supported, you can safely update properties or fields in a single elegant line.

3. Extension Members (Beyond Methods)

C# 14 brings “extension everything”: extension properties, indexers, and static members for existing types.

<code><em>// Before: Only extension methods allowed</em>
public static class MyExtensions
{
    public static bool IsEmpty(this IEnumerable<int> source)
        => !source.Any();
}

<em>// After: Extension members</em>
public static class Enumerable
{
    extension<T>(IEnumerable<T> source)
    {
        <em>// Extension Property</em>
        public bool IsEmpty => !source.Any();

        <em>// Extension Indexer</em>
        public T this[int index] => source.Skip(index).First();

        <em>// Extension Method</em>
        public IEnumerable<T> Filter(Func<T, bool> predicate) => source.Where(predicate);
    }

    extension<T>(IEnumerable<T>)
    {
        <em>// Static Extension Property</em>
        public static IEnumerable<T> Identity => Enumerable.Empty<T>();
    }
}

Usage:

<code>var nums = new[] { 2, 4, 6 };
bool empty = nums.IsEmpty; <em>// false</em>
int second = nums[1]; <em>// 4</em>
var zeros = Enumerable<int>.Identity; <em>// Empty int sequence</em>

4. Parameter Modifiers in Lambda Expressions

Previously, lambdas could not have parameter modifiers. Now, you can use scopedrefinout, and ref readonly:

<code><em>// Before</em>
delegate bool TryParse<T>(string text, out T result);
TryParse<int> parser = (text, out result) => int.TryParse(text, out result);

<em>// After (with parameter modifier)</em>
Func<string, ref int, bool> tryParse = (string s, ref int value) => int.TryParse(s, out value);

5. Implicit Span Conversions

Memory-efficient code is easier than ever! Arrays can be passed to Span<T> and ReadOnlySpan<T> parameters without explicit conversions.

<code>void ProcessData(ReadOnlySpan<int> data) { <em>/* ... */</em> }
<em>// Before</em>
ProcessData(new ReadOnlySpan<int>(array));

<em>// After</em>
ProcessData(array); <em>// now auto-converts</em>

Conclusion

C# 14 brings tangible benefits for everyday developers:

  • Less boilerplate, better validation: Quickly add logic to properties while retaining brevity.
  • Expression power: Write safer assignments and smarter extension APIs.
  • Performance help: Leverage span and lambda changes for better memory and result handling.

Whether maintaining legacy code or building new .NET 10 solutions, mastering these features makes you a modern C# expert.

How to Generate Sitemap XML in Sitecore Using PowerShell

If you manage a multi-site Sitecore environment, you’ve probably encountered the need to create sitemap.xml files for search engines like Google and Bing. While you can build this with C# code or third-party modules, an easier, zero-deployment approach is to use the Sitecore PowerShell Extensions (SPE) module.

In this guide, you’ll learn how to generate a Sitemap XML for multiple websites directly from the Sitecore admin interface using PowerShell. This approach is ideal for teams that do not have access to deploy code to content delivery (CD) servers or for situations where the original source code is missing.

Why Generate Sitemap XML Using PowerShell

Using Sitecore PowerShell to generate sitemap files provides several advantages:

  • No code deployment required; everything runs in the Content Management (CM) environment.
  • Supports multiple sites within one instance (multi-tenant setup).
  • Full control over URL structure, output path, and file naming.
  • Quick and repeatable; can be run on-demand or scheduled.
  • Produces search engine-friendly XML that complies with sitemaps.org.

PrerequisitesBefore running the script, ensure the following:

  1. Sitecore PowerShell Extensions (SPE) is installed.
  2. You can access PowerShell ISE within the Sitecore admin area (/sitecore/shell/Applications/PowerShell/PowerShellIse).
  3. The IIS App Pool user has write permissions to the folder where the sitemap XML will be saved.

The PowerShell Script

Below is the complete script that generates a combined sitemap.xml for multiple sites in your Sitecore instance.

# =======================================
# Sitecore Unified Sitemap Generator
# Combines multiple sites into one sitemap.xml
# =======================================

# Define all site configurations
$sites = @(
    @{
        RootPath = "master:/sitecore/content/sample-website/Home"
        Domain   = "https://www.sample-website.com"
    },
    @{
        RootPath = "master:/sitecore/content/another-site/Home"
        Domain   = "https://www.another-site.com"
    }
)

# Output path for the combined sitemap
$outputPath = "D:\WebApplications\sitecore\sitemap_combined.xml"

# Configure LinkManager options
$linkOptions = New-Object Sitecore.Links.UrlOptions
$linkOptions.AlwaysIncludeServerUrl = $false
$linkOptions.AddAspxExtension = $false
$linkOptions.LanguageEmbedding = "Never"

Write-Host "Starting unified sitemap generation..."

# Initialize XML document
$sitemap = New-Object System.Xml.XmlDocument
$urlset = $sitemap.CreateElement("urlset")
$urlset.SetAttribute("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
$sitemap.AppendChild($urlset) | Out-Null

# Function to traverse and append items
function Add-ItemToSitemap($item, $domain) {
    if ($item -ne $null -and $item.TemplateName -ne "Folder" -and $item.Fields["__Renderings"]) {

        # Generate relative URL
        $relativeUrl = [Sitecore.Links.LinkManager]::GetItemUrl($item, $linkOptions)

        # Strip CM hostname if present
        if ($relativeUrl -match "https?://[^/]+(/.*)") {
            $relativeUrl = $Matches[1]
        }

        # Build full CD URL
        if ($relativeUrl.StartsWith("/")) {
            $fullUrl = "$domain$relativeUrl"
        } else {
            $fullUrl = "$domain/$relativeUrl"
        }

        # Build <url> element
        $urlElement = $sitemap.CreateElement("url")

        $loc = $sitemap.CreateElement("loc")
        $loc.InnerText = $fullUrl
        $urlElement.AppendChild($loc) | Out-Null

        $lastmod = $sitemap.CreateElement("lastmod")
        $lastmod.InnerText = $item.Statistics.Updated.ToString("yyyy-MM-dd")
        $urlElement.AppendChild($lastmod) | Out-Null

        $urlset.AppendChild($urlElement) | Out-Null
    }

    foreach ($child in $item.Children) {
        Add-ItemToSitemap $child $domain
    }
}

# Iterate over each site and add its URLs
foreach ($site in $sites) {
    Write-Host "`n--------------------------------------------"
    Write-Host "Processing site: $($site.Domain)"
    Write-Host "Root path: $($site.RootPath)"
    Write-Host "--------------------------------------------`n"

    $rootItem = Get-Item -Path $site.RootPath
    if ($null -eq $rootItem) {
        Write-Error "Root item not found: $($site.RootPath)"
        continue
    }

    Add-ItemToSitemap $rootItem $site.Domain
}

# Save combined sitemap
$sitemap.Save($outputPath)

Write-Host "`nUnified sitemap generated successfully."
Write-Host "Output path: $outputPath"

How the Script Works

1. Site Configuration

The $sites array defines multiple websites (root paths and their public domains). You can add as many entries as your Sitecore instance hosts.

$sites = @(
    @{ RootPath = "master:/sitecore/content/sample-website/Home"; Domain = "https://www.sample-website.com" }
)

2. LinkManager Options

The script configures Sitecore’s LinkManager to generate clean, relative URLs without .aspx extensions or CM hostnames.

3. Recursive Traversal

The function Add-ItemToSitemap recursively loops through all Sitecore items under each root path and includes items that have renderings (actual pages).

4. URL Normalization

The script ensures that all URLs point to the content delivery (CD) domain, not the internal CM hostname, even though the script is executed in the CM environment.

5. XML Output

All URLs from all configured sites are merged into a single XML structure under <urlset> and saved to the defined $outputPath.

Example Output

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://www.sample-website.com/about-us</loc>
    <lastmod>2025-11-06</lastmod>
  </url>
  <url>
    <loc>https://www.another-site.com/blog/latest-article</loc>
    <lastmod>2025-11-06</lastmod>
  </url>
</urlset>

This sitemap can be submitted directly to Google Search Console, Bing Webmaster Tools, or any search crawler for indexing.

Summary

This PowerShell-based sitemap generator provides a simple, flexible, and deployment-free solution for Sitecore developers and administrators. It can be easily adapted for:

  • Single or multi-site Sitecore instances
  • Multilingual site structures
  • Automated nightly tasks using Sitecore’s Task Scheduler

By using this approach, you can maintain full control over how search engines crawl your websites without requiring code changes or deployments.

How to Configure Claude Code with Kimi K2, DeepSeek, and GLM: Complete WSL Setup Guide

Claude Code is a powerful CLI tool that can be configured to work with multiple AI providers beyond Anthropic’s default endpoints. In this comprehensive guide, you’ll learn how to set up Claude Code configuration with three popular AI providers: Kimi K2, DeepSeek, and GLM, all while using Windows Subsystem for Linux (WSL).

Prerequisites

  • Windows 10/11 with WSL installed
  • Claude Code CLI installed
  • API tokens for Kimi K2, DeepSeek, and/or GLM
  • Basic familiarity with bash commands

Why Use Multiple AI Providers with Claude Code?

Different AI providers offer unique advantages:

  • Kimi K2: Excellent for Chinese language processing and local deployment options
  • DeepSeek: Strong performance in coding tasks and mathematical reasoning
  • GLM: Optimized for conversational AI and general-purpose tasks

Step 1: Create Environment Files for Each AI Provider

First, we’ll create separate environment files for each AI provider to store their API configurations securely.

Creating the Kimi K2 Environment File

Create the ~/.claude-kimi-env file:

export ANTHROPIC_BASE_URL="https://api.moonshot.cn/v1"
export ANTHROPIC_AUTH_TOKEN="your_kimi_token_here"

Creating the DeepSeek Environment File

Create the ~/.claude-deepseek-env file:

export ANTHROPIC_BASE_URL="https://api.deepseek.com/v1"
export ANTHROPIC_AUTH_TOKEN="your_deepseek_token_here"

Creating the GLM Environment File

Create the ~/.claude-glm-env file:

export ANTHROPIC_BASE_URL="https://api.z.ai/api/anthropic"
export ANTHROPIC_AUTH_TOKEN="your_glm_token_here"

Step 2: Set Up Convenient Aliases

To make switching between AI providers seamless, we’ll create bash aliases that automatically load the correct environment and launch Claude Code.

Create or edit the ~/.bash_aliases file and add the following aliases:

# Custom aliases

  # Claude GLM alias
  alias claude-glm='source ~/.claude-glm-env && claude --dangerously-skip-permissions'

  # Claude Kimi alias
  alias claude-kimi='source ~/.claude-kimi-env && claude --dangerously-skip-permissions'

  # Claude DeepSeek alias
  alias claude-deepseek='source ~/.claude-deepseek-env && claude --dangerously-skip-permissions'

  # Add more aliases below this line

Step 3: Ensure Aliases Load Automatically in WSL

For the aliases to work every time you start WSL, verify that your ~/.bashrc file includes the following lines (they should be there by default):

if [ -f ~/.bash_aliases ]; then
      . ~/.bash_aliases
  fi

Step 4: Apply the Configuration

To use the new aliases immediately without restarting WSL, run:

source ~/.bash_aliases

How to Use Your New Claude Code Setup

Now you can easily switch between AI providers using simple commands:

  • claude-kimi – Launch Claude Code with Kimi K2
  • claude-deepseek – Launch Claude Code with DeepSeek
  • claude-glm – Launch Claude Code with GLM

Security Best Practices

Important Security Tips:

  • Never commit environment files to version control
  • Use strong, unique API tokens for each provider
  • Regularly rotate your API keys
  • Set appropriate file permissions: chmod 600 ~/.claude-*-env

Troubleshooting Common Issues

Aliases Not Working

If your aliases aren’t working after starting WSL:

  1. Check if ~/.bash_aliases exists
  2. Verify ~/.bashrc sources the aliases file
  3. Run source ~/.bashrc to reload the configuration

API Connection Issues

If you encounter API connection problems:

  • Verify your API tokens are correct
  • Check if the API endpoints are accessible from your network
  • Ensure the base URLs are properly formatted

Advanced Configuration Tips

Adding Model-Specific Parameters

You can extend your environment files to include model-specific parameters:

export ANTHROPIC_BASE_URL="https://api.deepseek.com/v1"
  export ANTHROPIC_AUTH_TOKEN="your_token"
  export ANTHROPIC_MODEL="deepseek-chat"

Creating Project-Specific Configurations

For different projects, you might want different AI providers. Consider creating project-specific environment files and aliases.

Conclusion

You’ve successfully configured Claude Code to work with multiple AI providers on Windows WSL. This setup gives you the flexibility to choose the best AI provider for each task while maintaining a consistent development workflow.

The combination of environment files and bash aliases provides a clean, secure, and efficient way to manage multiple AI provider configurations. Whether you’re working with Kimi K2’s Chinese language capabilities, DeepSeek’s coding expertise, or GLM’s conversational strengths, you can now switch between them effortlessly.

How I Set Up My AI Development Environment in WSL on Windows with an RTX 5070 Ti

Introduction

Over the past few weeks, I’ve been diving into AI model training and inference using open-source GPT-style models. I wanted a setup that could take advantage of my NVIDIA RTX 5070 Ti for faster experimentation, but still run inside WSL (Windows Subsystem for Linux) for maximum compatibility with Linux-based tools.

After a bit of trial and error — and a couple of GPU compatibility hurdles — I now have a fully working environment that runs Hugging Face models directly on my GPU. Here’s exactly how I did it.

1. Installing WSL and Preparing Ubuntu

I started by making sure WSL2 was installed and running an Ubuntu distribution:

wsl --install -d Ubuntu
wsl --set-default-version 2

Then I launched Ubuntu from the Start Menu, created my user, and updated everything:

sudo apt update && sudo apt -y upgrade

2. Enabling GPU Support in WSL

Since I wanted GPU acceleration, I installed the latest NVIDIA Game Ready/Studio Driver for Windows. This is important because WSL uses the Windows driver to expose the GPU inside Linux.

Inside WSL, I checked GPU visibility:

nvidia-smi

If you see your GPU listed, you’re good to go.

3. Installing Micromamba for Environment Management

I like to keep my AI experiments isolated in separate environments, so I use micromamba (a lightweight conda alternative).

First, I installed bzip2 (needed for extracting micromamba):

sudo apt install -y bzip2

Then downloaded and initialized micromamba:

cd ~
curl -L https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj
./bin/micromamba shell init -s bash -r ~/micromamba
exec $SHELL

4. Creating a Python Environment

I created an environment named llm with Python 3.11:

micromamba create -y -n llm python=3.11
micromamba activate llm

5. Installing PyTorch with RTX 5070 Ti Support

Here’s where I hit my first big roadblock. The PyTorch stable builds didn’t yet support my compute capability 12.0 (Blackwell architecture). The fix was to install the nightly cu128 build of PyTorch, which does include sm_120 support:

pip install transformers datasets accelerate peft bitsandbytes trl sentencepiece evaluate

Transformers – Hugging Face’s main library for working with pre-trained models (like GPT, BERT, etc.), including easy APIs for loading, running, and fine-tuning them.

Datasets – A fast, memory-efficient library for loading, processing, and sharing large datasets used in machine learning.

Accelerate – A tool from Hugging Face that makes it simple to run training across CPUs, GPUs, or multiple devices with minimal code changes.

PEFT (Parameter-Efficient Fine-Tuning) – A library for applying lightweight fine-tuning methods like LoRA so you can adapt large models without retraining all parameters.

Bitsandbytes – A library for quantizing models (e.g., 8-bit, 4-bit) to save memory and speed up inference/training, especially on GPUs.

TRL (Transformers Reinforcement Learning) – Hugging Face’s library for training transformer models with reinforcement learning techniques like RLHF (Reinforcement Learning from Human Feedback).

SentencePiece – A tokenizer library that helps split text into subword units, especially useful for multilingual and large-vocabulary models.

Evaluate – A library to easily compute machine learning metrics (like accuracy, BLEU, ROUGE, etc.) in a standardized way.

6. Installing AI Libraries

With PyTorch sorted, I installed the Hugging Face ecosystem and related tools:

pip install transformers datasets accelerate peft bitsandbytes trl sentencepiece evaluate

7. Testing GPU Inference

To confirm everything worked, I ran a small model on GPU:

from transformers import AutoTokenizer, pipeline

model_id = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tok = AutoTokenizer.from_pretrained(model_id)
pipe = pipeline("text-generation", model=model_id, tokenizer=tok, device_map="auto")

print(pipe("Explain LoRA in one sentence.", max_new_tokens=50)[0]["generated_text"])

The output came back quickly — and my GPU usage spiked in nvidia-smi — a great sign that everything was working.

8. Conclusion

With this setup, I can run and fine-tune open-source GPT models entirely on my RTX 5070 Ti inside WSL. It’s a clean, isolated environment that avoids Windows-specific headaches and keeps me close to the Linux ecosystem most AI tooling is built for.

If you’re working with a newer NVIDIA GPU, don’t be surprised if you need to grab nightly builds until stable releases catch up. Once you do, you’ll be able to enjoy the full speed of your hardware without leaving the comfort of Windows.

Explore Key Features of C# 14 for Developers

Comprehensive Analysis of C# 14: Key Features and Enhancements

  • C# 14 introduces significant features for enhancing developer productivity and performance.
  • Key enhancements include implicit span conversions, extended `nameof` capabilities, and lambda expression improvements.
  • New features like the contextual `field` keyword and partial constructors promote modular design and cleaner code.
  • User-defined compound assignment operators and dictionary expressions improve performance and simplify code.
  • C# 14 focuses on memory safety, streamlined syntax, and community-driven enhancements.

Table of Contents

Enhanced Span Support for Memory Optimization

One of the standout features of C# 14 is the first-class support for System.Span<T> and System.ReadOnlySpan<T>, which is indicative of a broader emphasis on memory safety and performance optimization in high-efficiency scenarios such as real-time data processing and resource-constrained environments. The introduction of implicit conversions between spans and arrays significantly simplifies the handling of memory-intensive operations, allowing developers to manage memory more effectively without incurring the overhead associated with manual marshaling.

For instance, when converting a string array to a ReadOnlySpan<string>, C# 14 allows a seamless assignment:

string[] words = { "Hello", "World" };
ReadOnlySpan<string> span = words; // Implicit conversion

This change leverages runtime optimizations to minimize heap allocations, thereby making spans ideal for performance-critical applications, such as game development or Internet of Things (IoT) scenarios where every byte of memory counts. Furthermore, as the compiler has been enhanced to recognize span relationships natively, developers can now utilize spans as extension receivers and benefit from improved generic type inference, streamlining their development experience.

Extended `nameof` Capabilities for Reflection

In an increasingly complex programming landscape, understanding type names dynamically becomes essential. C# 14 enhances the capabilities of the nameof operator by enabling it to resolve unbound generic type names. This represents a significant simplification over previous versions, where types like List<int> would return cumbersome names such as “List`1”. With the new feature, invoking nameof(List)<> cleanly returns “List”.

This enhancement is particularly beneficial in the context of frameworks that rely heavily on reflection, such as serialization libraries and dependency injection containers. For example, when building error messages for generic repositories, the use of nameof can greatly improve maintainability and readability:

throw new InvalidOperationException($"{nameof(IRepository<>)} requires implementation.");

By reducing the clutter in logs caused by arity notation, developers can focus on more meaningful output, significantly improving debugging efforts and enhancing the overall developer experience. Feedback from the C# community has been instrumental in shaping this capability, as developers sought to minimize string literals in reflection-heavy code bases.

Lambda Expressions with Parameter Modifiers

C# 14 brings scalability and clarity to lambda expressions by allowing them to incorporate parameter modifiers such as ref, in, out, scoped, and ref readonly, all without needing to specify parameter types explicitly. Prior to this enhancement, developers often faced cumbersome syntax when defining output parameters, which detracted from code readability and conciseness.

The following example illustrates how this feature simplifies lambda expressions:

delegate bool TryParse<T>(string text, out T result);
TryParse<int> parse = (string s, out int result) => int.TryParse(s, out result);

This can now be rewritten more cleanly in C# 14 as:

TryParse<int> parse = (s, out result) => int.TryParse(s, out result);

The absence of explicit type annotations improves the fluency of the code, making it easier to read and write and aligning with existing lambda functionality. However, it is essential to note that modifiers like params still require explicit typing due to compiler constraints. This enhancement particularly benefits low-level interoperability scenarios where output parameters are frequently utilized, as it reduces boilerplate code and fosters a more fluid coding experience.

Field Keyword in Auto-Implemented Properties

C# 14 introduces the contextual field keyword, which greatly streamlines the handling of auto-implemented properties by granting direct access to compiler-generated backing fields. This improvement is particularly notable in scenarios requiring null validation or other property logic, which traditionally necessitated verbose manual backing field management.

Consider this example from prior versions:

private string _message;

public string Message
{
    get => _message;
    set => _message = value ?? throw new ArgumentNullException(nameof(value));
}

With C# 14, developers can eliminate redundancy by utilizing the new field keyword:

public string Message
{
    get;
    set => field = value ?? throw new ArgumentNullException(nameof(value));
}

Here, field acts as a placeholder for the implicit backing field, enhancing readability and maintainability while preserving encapsulation principles. However, users must remain mindful of potential symbol collisions, as using field as an identifier within class members requires disambiguation (e.g., @field or this.field).

This change not only aids in reducing boilerplate but also encourages more concise property implementations, ultimately resulting in cleaner, more maintainable code across projects.

Partial Events and Constructors for Modular Design

With the expanding complexity of software architectures, C# 14 introduces partial events and constructors, which enhance code modularity and facilitate a more organized approach to large codebases. By allowing event and constructor definitions to be split across multiple files, developers can structure their code more flexibly and responsively.

For instance, when defining a logger class, developers can now separate the event declaration and implementation:

// File1.cs
public partial class Logger
{
    public partial event Action<string>? LogEvent;
}

// File2.cs
public partial class Logger
{
    public partial event Action<string>? LogEvent
    {
        add => Subscribe(value);
        remove => Unsubscribe(value);
    }
}

This flexibility extends to partial constructors as well, enabling developers to distribute initializer logic across different files. While only one declaration can employ primary constructor syntax (e.g., Logger(string source)), this capability fosters enhanced collaboration and better organization within teams working on large-scale applications or utilizing code generation tools.

The implications of this feature are significant for source generation and modern architecture patterns, where the separation of concerns and maintainability are paramount. By allowing tool-generated code to inject validation and initialization logic into user-defined constructors, this enhancement streamlines workflows and supports the continuous evolution of application architectures.

Extension Members for Augmenting Types

As a preview feature, C# 14 will revolutionize the way developers can augment existing types through extension members, allowing the addition of properties and static members alongside the traditional extension methods. This capability leads to a more intuitive and discoverable syntax, particularly beneficial for extending closed-source or interface-based types without necessitating complex inheritance setups.

For example, adding an IsEmpty property to the IEnumerable<T> interface can now be accomplished straightforwardly:

public extension EnumerableExtensions for IEnumerable<T>
{
    public bool IsEmpty => !this.Any();
}

This syntax not only enhances clarity but also promotes code reuse and modularity:

if (strings.IsEmpty) return;

In addition, static extension members bolster usability and flexibility when dealing with types that developers cannot directly modify. The implications for team projects and libraries are substantial, as this feature allows for richer connectivity across application codebases while preserving the integrity of existing types.

The extension member functionality is part of an ongoing effort to make C# more expressive and adaptable, fulfilling developers’ needs for extended functionality while maintaining core principles of object-oriented programming. As this feature matures, developers can look forward to an enriched language experience that aligns more closely with modern programming paradigms.

Null-Conditional Assignment for Safe Mutation

Null safety continues to be a core concern in modern development, and C# 14 introduces a compelling enhancement to null-conditional operators: they can now be utilized on assignment targets. This evolution allows for more concise syntax and safer code execution, as the language can now intelligently bypass assignments for null objects without requiring explicit null checks.

For example, prior to C# 14, developers would write:

if (customer != null) customer.Order = GetOrder();

With the introduction of null-conditional assignment, this logic simplifies to:

customer?.Order = GetOrder();

In this case, if customer is null, the assignment of Order is gracefully skipped, significantly reducing overhead for conditional checks. This also applies to indexed assignments, as shown in the following example:

dict?["key"] = value; // Assigns only if dict is non-null

While these enhancements integrate seamlessly into existing null-coalescing patterns, it is crucial to note the limitations; for instance, chaining assignments (e.g., obj?.A?.B = value) will result in compile-time failures if any intermediary references are null. Nonetheless, this feature represents a significant step forward in safeguarding against null reference exceptions, enhancing the overall reliability of C# applications.

User-Defined Compound Assignment Operators

One of the most innovative features in C# 14 is the ability for developers to overload compound assignment operators such as += and -=. This grants developers the ability to optimize performance during mutation operations by directly altering existing objects rather than creating new instances, which is especially beneficial in high-efficiency contexts like mathematical computations.

For instance, a matrix class could utilize user-defined compound operators as follows:

public class Matrix
{
    public double[,] Values; // Matrix values
    
    public void operator +=(Matrix other)
    {
        for (int i = 0; i < Rows; i++)
            for (int j = 0; j < Cols; j++)
                Values[i, j] += other.Values[i, j];
    }
}

This syntax supports in-place mutations, avoiding the need for redundant memory allocations, which can be critical when dealing with large data structures. Notably, the operator must adhere to specific constraints, returning void and omitting static modifiers, due to its in-place nature, enforcing consistency with language rules to prevent unexpected behavior.

Through the strategic utilization of user-defined compound assignment operators, developers can achieve significant performance gains, with benchmarks indicating up to 40% fewer allocations in computation-intensive workloads. This capability empowers high-performance applications to operate seamlessly under heavy load, enhancing the robustness of numerical algorithms and data processing workflows.

Dictionary Expressions and Collection Enhancements

While still in development, C# 14 introduces the concept of dictionary expressions, poised to revolutionize how developers initialize dictionaries. This feature aims to provide an intuitive syntax akin to other collection initializers, allowing for cleaner and more concise code:

Dictionary<string, int> ages = ["Alice": 30, "Bob": 35];

This syntax reduces typing overhead and enhances readability compared to traditional dictionary initialization methods. Additionally, simultaneous enhancements to collection expressions allow for optimized initialization of collections, enabling more efficient operations during startup phases.

For example, using collection expressions like [1, 2, ..existing] can lead to improved startup performance due to internal optimizations that minimize individual Add calls. These enhancements collectively serve to streamline the coding experience, enabling developers to focus on core logic rather than boilerplate initialization code and improving the overall performance of applications.

Compiler Breaking Changes and Adoption Guidance

With any significant language update, developers must navigate breaking changes to ensure smooth transitions to new features. C# 14 introduces specific alterations that warrant careful attention. One notable change is the treatment of the scoped modifier in lambda expressions, which has transitioned into a reserved keyword. This shift necessitates the use of the @ sign for identifiers previously named scoped:

var v = (scoped s) => { ... }; // Error: 'scoped' is reserved

In this case, developers should use @scoped if they need to reference that identifier.

Moreover, the new implicit span conversions may introduce ambiguities in overload resolution, especially in scenarios involving method overloading between Span<T> and standard arrays. To mitigate this risk, developers should employ explicit casting to .AsSpan() or utilize the OverloadResolutionPriorityAttribute to guide the compiler on intended overload selections.

To ensure a successful transition, developers are advised to conduct thorough testing with the .NET 10 SDK and address any warnings or breaking changes using #pragma directives or by carefully managing type disambiguations. This proactive approach will facilitate embracing C# 14’s enhancements while maintaining robust codebases.

Conclusion: Strategic Impact and Future Directions

In summary, C# 14 embodies a substantial leap forward in refining the C# language, equipping developers with enhanced language ergonomics and performance-oriented features. The focus on implicit spans, improved null handling, and the introduction of the contextual field keyword significantly aligns with modern development paradigms that prioritize memory safety and streamlined syntax.

Developers should consider incorporating recommendations such as adopting the field keyword to reduce boilerplate in property handling, leveraging partial events and constructors in extensive codebases, and conducting audits on compound operators within numerical computation libraries to uncover allocation hotspots.

Looking ahead, as the ecosystem surrounding C# evolves, further iterations may finalize features like dictionary expressions and expand support for both static and instance extension members. As teams stabilize their tooling around .NET 10, placing a priority on these enhancements will empower their applications to excel within a rapidly advancing technological landscape. Emphasizing a balance between preview features and production stability will be crucial as organizations seek to capitalize on the opportunities presented by C# 14 and beyond.

FAQs

What are the main enhancements in C# 14?
C# 14 introduces significant improvements like implicit span conversions, extended capabilities for the nameof operator, and enhancements to lambda expressions, among others, aimed at improving developer productivity and code quality.

How does C# 14 improve memory management?
With the first-class support for System.Span<T> and the introduction of null-conditional assignment, C# 14 optimizes memory handling by reducing heap allocations and simplifying null checks.

What should developers be cautious about with breaking changes?
Developers need to navigate changes such as the reserved status of the scoped modifier and potential ambiguities with implicit span conversions to ensure smooth transitions to C# 14.

Understanding COM Components in C# for Interoperability

COM Components in C#: Enabling Interoperability in .NET Applications

  • Understanding the Component Object Model (COM) is essential for seamless technology integration.
  • C# provides robust interop features to expose and consume COM components effortlessly.
  • Proper resource management in COM is crucial for ensuring efficient memory usage.
  • Integrating AI and automation can significantly enhance COM component functionality.
  • Managing visibility and threading models is key to successful COM implementations.

Table of Contents

What is COM?

The Component Object Model (COM), developed by Microsoft, is a binary software standard that facilitates inter-process communication and enables dynamic object creation across different programming languages. Its core goal is to provide a flexible and reusable approach to component development by defining a standard interaction mechanism:

  • Language-Agnostic: COM is not tied to any programming language, allowing components to be created and consumed in various languages, thus promoting wider interoperability.
  • Object-Oriented: COM components are organized around object-oriented principles, allowing for encapsulation, inheritance, and polymorphism.

COM is vital for various Microsoft technologies such as Object Linking and Embedding (OLE), ActiveX, and COM+.

Key Concepts of COM

1. COM Interfaces and Objects

In COM, interfaces form the backbone of interaction between clients and components. Each interface comprises a collection of abstract operations that promote loose coupling. The base interface, IUnknown, supports fundamental methods, including reference counting and interface querying via the QueryInterface mechanism. Each COM interface is uniquely identified by a UUID (Universally Unique Identifier), ensuring that clients interact with the correct versions of COM objects.

2. COM in C#: Interoperability

C# provides robust support for consuming and exposing COM components through interop features. This allows for seamless interaction between native COM components and managed .NET code.

Exposing a C# class to COM requires several steps:

  1. Declare Public Interface: Define a public interface that lists the methods and properties that will be accessible to COM clients.
  2. Use COM Attributes: Apply attributes like [ComVisible(true)] and [Guid("...")] to mark classes and interfaces for import into the COM system.
  3. Register Assembly: Set your assembly to “Register for COM Interop” in project properties, allowing it to register with the Windows registry.

Example Implementation

Let’s explore a simple example of how to create a COM component in C#. Here, we will create a basic calculator that can be accessed via COM.

Step 1: Define the Interface

using System.Runtime.InteropServices;

namespace CalculatorCOM
{
    [Guid("12345678-abcd-efgh-ijkl-123456789012")]
    [ComVisible(true)]
    public interface ICalculator
    {
        double Add(double a, double b);
        double Subtract(double a, double b);
    }
}

Step 2: Implement the Interface

using System.Runtime.InteropServices;

namespace CalculatorCOM
{
    [Guid("87654321-lkjh-gfed-cba-210987654321")]
    [ComVisible(true)]
    public class Calculator : ICalculator
    {
        public double Add(double a, double b) => a + b;

        public double Subtract(double a, double b) => a - b;
    }
}

Step 3: Register for COM Interop

In your project properties, check the “Register for COM Interop” option. After building the project, the COM component will be available for use in any COM-compatible environments.

Managing COM Lifetime and Activation

COM components are not statically linked; they are activated on-demand at runtime. Clients can create instances of COM objects using system APIs like CoGetClassObject and CreateInstance. Proper resource management relies on the explicit release of object references to ensure that memory and resources are correctly freed.

Activation Example

Below is a simple C# client code demonstrating how to use the Calculator COM object:

class Program
{
    static void Main()
    {
        Type calculatorType = Type.GetTypeFromProgID("CalculatorCOM.Calculator");
        dynamic calculator = Activator.CreateInstance(calculatorType);
        
        double resultAdd = calculator.Add(5.0, 10.0);
        double resultSubtract = calculator.Subtract(15.0, 5.0);
        
        Console.WriteLine($"Addition Result: {resultAdd}");
        Console.WriteLine($"Subtraction Result: {resultSubtract}");
    }
}

Common Pitfalls and Best Practices

  • Visibility: Only public members in the interface are visible to COM clients. Members defined in the class, but not the interface, will remain hidden from COM consumers.
  • Multiple Interfaces: A class can implement multiple interfaces. The first interface marked in the class definition is treated as the default interface for COM.
  • Threading Models: Be aware of the threading models used by COM. Ensure that your components are safe in multi-threaded contexts, particularly if accessed across threads.

Integrating AI and Automation

In the rapidly evolving tech landscape, integrating AI and automation into COM components can enhance their functionality. For example, using OpenAI’s models, you could develop intelligent components that provide insights or automate complex workflows. This would not only modernize legacy systems but also increase their value and usability in contemporary applications.

Conclusion

COM components in C# present powerful opportunities for cross-language and cross-process communication. Understanding their structure, implementation, and the critical role they play in interoperability can significantly enhance your software solutions. By exposing .NET classes to COM, you can unlock the potential of legacy systems while positioning your applications for future innovation.

For further implementation examples and insights, feel free to explore my GitHub.

Also, connect with me on LinkedIn, where I share additional resources on software architecture and engineering practices.

FAQ

Page 1 of 20

Powered by WordPress & Theme by Anders Norén