❌

Normal view

The Microsoft agent framework wars are over. The real architecture decision starts now

24 July 2026 at 10:00

Over the past year, I had the same conversation with almost every team starting an AI initiative. Should we build on Semantic Kernel, AutoGen or Foundry?

At first it felt like the most important architectural decision we’d make. Each framework had its own philosophy, each promised to be the foundation for enterprise AI, and picking the wrong one felt like an expensive mistake. I spent a lot of time helping teams weigh the trade-offs.

Looking back, I think we were asking the wrong question. I certainly was.

I watched teams spend months debating SDKs while the decisions that actually decided whether their applications survived production went unexamined. Some built elaborate orchestration layers for workflows that a few deterministic functions would have handled. Others avoided agent frameworks entirely and later found they’d designed themselves into a corner.

Then Microsoft settled it for us. It introduced the unified Agent Framework, quietly moved Semantic Kernel and AutoGen into maintenance mode, and the debate I’d spent months refereeing was suddenly over. Turns out the answer to “which of the three” was “none of the three, here’s a fourth.” The framework hit version 1.0 and general availability in April 2026, stable across .NET and Python.

What surprised me wasn’t the decision. It was how fast a debate that had eaten so much of our attention stopped mattering. Microsoft changed the menu.

It didn’t change the meal.

The framework was never the hard part

Framework selection dominated almost every early conversation I had about enterprise agents. Which SDK do we standardize on? Which orchestration model gives us the most flexibility? Which one is Microsoft actually betting on?

Fair questions. But after a year of watching these projects play out, I’ve slowly come around to a different view. Those weren’t the questions that decided anything.

The first question I ask now is much smaller. Does this thing actually need an agent?

It sounds obvious, and I still get it wrong sometimes. But it’s the mistake I see most. On one project, a team spent weeks designing a multi-agent workflow for a process that ran the same four steps every time: read a document, validate it, call an API, send a notification. The diagrams looked great. The system in production didn’t. A few well-tested functions would have been easier to build, easier to maintain and a lot easier to trust.

Part of this is just that “agent” has become the word everyone reaches for. Sometimes it’s the right call. Sometimes it’s a workflow we already knew how to build, wearing a newer label. An agent earns its complexity when it genuinely has to decide things you can’t predetermine, choosing between tools, adapting to what it finds, working out its own next step. If you already know every step, you have a workflow, and a workflow is usually the better engineering choice. The consolidation didn’t change that. It just made it easier to see.

What building production agents actually taught me

Once I stopped fixating on frameworks, the same three problems kept showing up. None of them had anything to do with the SDK.

Context beats model choice

Early on I spent a lot of time comparing models, the way you’d agonize over a restaurant menu and then order what you always order. Now I spend most of it thinking about context, which is far less fun and far more useful.

I’ve watched good models fail because they were handed too much, not too little. One team I worked with gave the model access to nearly every internal document they had on the theory that more information meant better answers. It went the other way. Responses got slower, less consistent and sometimes skipped right past the thing that actually mattered. When we cut the context down to only what the task needed, the quality jumped almost immediately. I didn’t predict that. It taught me to be suspicious of “just give it everything.”

The best agent systems I’ve worked on weren’t the ones with the biggest context windows. They were the ones careful about what reached the model, and when. That’s not something the framework hands you.

Failure is where the real work is

Most agent demos look great because they’re built around the happy path. Production doesn’t extend that courtesy.

I remember a project where everything held up in testing. Then a downstream API timed out after the agent had already completed several earlier steps. We couldn’t just restart, because part of the business process had already gone through. We ended up spending far more time on recovery logic than we ever spent on prompts. That project changed how I think about this work. The hard part was never getting the model to make a decision. It was making sure the system didn’t fall apart when reality refused to follow the script.

Tool calls fail partway through. APIs return inconsistent data. Models call the same tool over and over because the last answer wasn’t what they wanted. That’s not the exception; that’s a normal Tuesday. Whether you retry, roll back, pause for a human or push on with partial results is a judgment call, and no framework is going to make it for you.

Identity is the real security boundary

This one surprised me most. The moment an agent stops being a chatbot and starts touching real business systems, identity matters more than orchestration.

Every project gets to the same question eventually. Who is this agent actually acting as? The developer’s credentials? A service account? The user who asked? Get it wrong and you’ve built something autonomous running with more access than any single person should have, which is exactly the kind of thing that looks fine until an audit. The Agent Framework, like most modern tooling, makes it easier to wire agents to tools through standards like the Model Context Protocol. That helps. But where human approval belongs, what needs extra authorization, how much rope to give the thing, those are still yours to decide.

The surprises weren’t technical

Here’s what I didn’t see coming. The hardest part of last year wasn’t technical at all. It was organizational. The moment a team heard “agent,” expectations shifted under everyone’s feet. Business stakeholders started expecting full autonomy. Developers assumed the thing could reason its way through anything. People started designing for flexibility before we’d even agreed on what problem we were solving. The word did damage before any code did. I found myself spending as much time resetting expectations as I did discussing architecture.

Build for change, not for today’s winner

I don’t think the teams that struggled last year picked the wrong framework. Semantic Kernel was reasonable. AutoGen was reasonable. Foundry made sense for plenty of cases. I’d have signed off on any of them.

The ones that got hurt put all their eggs in one framework, treating it as the foundation of the whole system instead of as one more dependency. Microsoft provided a migration path. But teams that had tightly coupled their applications to framework-specific abstractions discovered that migrating and rewriting are not the same thing. That wasn’t Microsoft’s doing. It was their own architecture’s. The teams that moved easily had kept their business logic, prompts and orchestration loose enough to evolve independently of any one SDK. For them, the change was a manageable project, not a teardown.

For what it’s worth, nobody I work with is treating this as an emergency. Most are moving the smaller workloads first, watching how they behave and leaving the production-critical systems alone until they actually understand the new abstractions. That’s the right instinct. And I doubt this is the last consolidation we’ll see, the ecosystem is still young, frameworks will keep absorbing each other and over time the differences between them will be operational more than architectural.

I don’t regret the framework debates, honestly. They were reasonable at the time. What changed wasn’t Microsoft’s roadmap.

It was mine. Watching these systems run in production taught me that the framework is the easiest piece to swap out. Recovery logic, context management, security boundaries, the business workflow itself, those stay with you long after today’s SDK gets replaced by tomorrow’s.

So, Microsoft made one decision easier by turning three frameworks into one. Good. Five years from now we’ll be on different tools, and we’ll still be asking the same handful of questions.

Does this actually need an agent? Does it have the right context? Can it recover when something breaks, because something will? Is it acting as the right person?

Those questions outlast every rewrite. That’s where I’ve learned to put my effort.

Frameworks come and go. Good architecture has to survive all of them.

This article is published as part of the Foundry Expert Contributor Network.
Want to join?

How to execute queries in parallel using EF Core

24 July 2026 at 10:00

EF Core is Microsoft’s flagship ORM (object-relational mapper), the software layer that allows .NET developers to work with relational databases. The DbContext class is the core component of the EF Core framework for managing database operations. However, the DbContext class in EF Core is not thread-safe. Hence, if you share DbContext instances between multiple threads, you will often encounter data corruption issues and the InvalidOperationException.

In this article, we’ll learn how we can execute queries in parallel in EF Core by handling thread-safety issues to avoid concurrency errors. To work with the code examples provided in this article, you should have Visual Studio 2026 installed in your system. You can download Visual Studio 2026 here.

Executing EF Core queries in parallel – the problem

When working in today’s data-driven applications, you will often need to fetch data from multiple unrelated datasets. In applications that use concurrency, thread-safety is critical to guaranteeing correct execution, avoiding data corruption and race conditions, and ensuring data consistency. Let’s understand this with an example.

Let’s say we want to populate a dashboard that displays all recently processed orders, metrics, logs, and traces, as well as your application’s performance metadata. We might write the following code.

public class Dashboard
{
    public List Orders { get; set; } = new();
    public Metrics Metrics { get; set; } = new();
    public List Logs { get; set; } = new();
    public List Traces { get; set; } = new();
}
public static async Task LoadDashboardAsync(ProductService productService)
{
    Task<List>    ordersTask  = productService.GetProcessedOrdersAsync();
    Task        metricsTask = productService.GetMetricsAsync();
    Task<List> logsTask    = productService.GetRecentLogsAsync();
    Task<List>    tracesTask  = productService.GetTracesAsync();
    await Task.WhenAll(ordersTask, metricsTask, logsTask, tracesTask);
    return new Dashboard
    {
        Orders  = await ordersTask,
        Metrics = await metricsTask,
        Logs    = await logsTask,
        Traces  = await tracesTask
    };
}

In the preceding code snippet, there are four read operations that are executed by four different Task instances. Our objective is to ensure that the database round trips run in parallel instead of in sequence. We can accomplish this by usingTask.WhenAll, which starts the four tasks, waits for every task to finish, then returns the data wrapped inside a new Dashboard instance.

If we executed these queries sequentially, the user would have to wait until each query completed its execution in turn—for a total wait time equal to the sum of the times for all four queries. However, by running these queries in parallel, we reduce the wait time considerably. The user will need to wait only as long as it takes for the slowest of the four queries to complete its execution.

However, there is a danger with the above approach. If you run multiple operations on the same DbContext instance, you will see an InvalidOperationException with the following message:

A second operation started in this context before the previous operation was completed. This is usually caused by multiple threads using the same DbContext instance; instance members are not guaranteed to be thread-safe.

Databases such as SQL Server, PostgreSQL, and Oracle Database follow a request-response communication model at the connection level: a single connection can process only one command at a time. Hence, you cannot run multiple queries concurrently using the connection. If you await several operations using the same connection, EF Core detects the overlapping use of a non-thread-safe context and throws an InvalidOperationException. To run queries in parallel, you must give each task its own connection or context.

Why DbContext isn’t thread-safe – and how to work around it

The DbContext class in EF Core is designed to manage a single unit of work. To be more precise, EF Core does not provide support for running multiple operations on the same DbContext instance. This design approach creates inherent challenges when you use the same DbContext instance across multiple threads. If DbContext were thread-safe, extensive locking would be required, which would degrade data access performance.

This stateful design of DbContext makes it unsuitable for concurrent access patterns that involve loading, modifying, or tracking different sets of data simultaneously, because it needs to maintain the internal representation of database state.

The change tracker is one of the most important components of DbContext in EF Core. It monitors all entities loaded into memory and detects any changes made to them after they have been loaded. It keeps track of the original, current, and changed values of the entities, thereby enabling the EF Core runtime to know the current state of these entities when you call the SaveChanges() method on the DbContext instance.

To implement thread-safety when working with DbContext, we must write our code to ensure that each concurrent operation gets its own copy of a short-lived instance. Now, we could accomplish this by wrapping a shared DbContext instance inside a thread-safe block using the lock keyword, so that all calls to the database take place using one and only one thread at a time. This approach is illustrated in the code snippet below.

using Microsoft.EntityFrameworkCore;
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions options) : base(options) { }
    public DbSet Products => Set();
}

However, while the above approach gives us the thread-safety we need, it can degrade data access performance considerably. A better approach is to use IDbContextFactory , which creates fresh DbContext instances on demand. Calling its CreateDbContext() method is cheap and produces a fresh, isolated context every time.

The following code snippet shows how you can register an instance of type IDbContextFactory as a singleton. You can safely call this code from any thread.

builder.Services.AddDbContextFactory(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("Default")));

Executing EF Core queries in parallel – the solution

Now let’s see how we can put IDbContextFactory to work. The following code illustrates a class named ProductService that uses a factory to create DbContext instances for each scope of work.

public class ProductService
{
    private readonly IDbContextFactory _factory;
    public ProductService(IDbContextFactory factory)
        => _factory = factory;
    public async Task GetByIdAsync(int id)
    {
        await using var context = await _factory.CreateDbContextAsync();
        return await context.Products.FindAsync(id);
    }
    public async Task UpdateStockQuantityAsync(int id, int updateQuantity)
    {
        await using var context = await _factory.CreateDbContextAsync();
        var product = await context.Products.FindAsync(id);
        if (product is null) return;
        product.Quantity += updateQuantity;
        await context.SaveChangesAsync();
    }
}

Note that ProductService has two methods, GetByIdAsync and UpdateStockQuantityAsync. An instance of the DbContext class is created locally in each of these methods. Now, suppose you have two threads, T1 and T2, that execute these methods concurrently. That is, thread T1 executes the GetByIdAsync method while thread T2 executes the UpdateStockQuantityAsync method. Because each of these methods is executed in isolation, they will have their own context, connection, and change-tracking information, and there will be no mutable state, so you don’t need to implement thread synchronization in either of these methods.

Consider the following code that executes a read operation and an update operation in two separate tasks.

public static async Task RunMethodsInParallelAsync(ProductService productService)
{
      Task readTask = productService.GetByIdAsync(1);
      Task updateTask = productService.UpdateStockQuantityAsync(3, 5);
      await Task.WhenAll(readTask, updateTask);
      Product? product = await readTask;
 }

The Task.WhenAll method runs the two tasks in parallel and waits until both have finished. The reason this approach is thread-safe, and will not create concurrency errors, is that each of these two methods creates its own DbContext instance internally. Therefore the read operation and the update operation use independent DbContext instances.

Use DbContext pooling to reduce allocation cost

Although creating DbContext instances is not that costly, you should consider using pooled contexts in applications that require high scalability and high performance. The following code snippet shows how you can register a pooled context.

builder.Services.AddPooledDbContextFactory(options =>
    options.UseSqlServer(connectionString));

A call to AddDbContext() will register a DbContext instance as scoped per HTTP request. Each request will run on a different thread and each will have its own context. However, keep in mind that the default scoped registration of the DbContext will not always suffice.

You will need a factory to create instances of DbContext when you’re using a background service, or performing some work inside a particular request, or running some business logic operation over multiple contexts.

Key takeaways

  • If you use EF Core in the data access layer of your application, you must implement thread safety measures whenever you run your queries in parallel.
  • You cannot execute multiple queries in parallel in EF Core using the same DbContext instance.
  • The IDbContextFactory enables you to create a DbContext instance for each thread, thereby enabling you to work with these instances in isolation.
  • Although using a DbContext pool involves a small allocation overhead, it becomes a non-issue if you need high throughput.

G# language for .NET borrows from Go, Kotlin, and Swift

23 July 2026 at 02:27

G# (GSharp) is moving forward as a programming language for Microsoft’s .NET platform, touted as bringing Go-, Kotlin-, and Swift-style ergonomics to the CLR (Common Language Runtime). The language is described by its creators as modern, simple, and accessible.

Although pre-1.0 and still growing, G# aims to be for people who want a small, predictable language with direct access to the .NET ecosystem. Developers will see imports, func, structs, slices, maps, channels, go, select, and for in iteration. Also important are nullable flow, direct calls into the CLR (Common Language Runtime), and built-in concurrency.

With G#, copyrighted in 2026, developers get value-oriented structs, reference-oriented classes, data structs, and data classes. For concurrency, G# uses scope for structured concurrency, async func and await for task-based asynchrony, and async sequence[T] for asynchronous streams. G# also makes use of the same Task and Task[T] types familiar from the .NET BCL (Base Class Library).

G# documentation is available on the GitHub site of Microsoft software engineer David Obando. “Every .NET type—your packages, third-party NuGet packages, the BCL—is callable from G# with the syntax you already know. CLR generics use G#’s bracket spelling, and method calls, properties, indexers, and for in over IEnumerable[T] all just work,” according to the website.

A Visual Studio Code extension for G3 can be found at marketplace.visualstudio.com. The extension adds syntax highlighting, language server features, build/run commands, and debugger configuration for .gs and .gsproj files. Developers can install the extension from within VS Code (search for “G#” in the Extensions view) or from the command line.

Microsoft releases .NET 11 Preview 6

15 July 2026 at 22:32

Preview 6 of Microsoft’s .NET 11 software development platform is now available, with improvements across the runtime, libraries, frameworks, and C# and F# programming languages.

Announced July 14, .NET 11 Preview 6 can be downloaded from dotnet.microsoft.com. The full, general production release is expected in November. The first preview was unveiled February 10.

Runtime-async, the .NET runtime’s built-in implementation of async/await, keeps getting faster in Preview 6, building on the suspension improvements from Preview 5. Async continuations can now opt out of ExecutionContext capture and restore. And the JIT now compiles a dedicated runtime-async version of a synchronous, task-returning method rather than delegating to it through a thunk. The JIT turns the method’s tail calls into runtime-async calls and awaits the task that would otherwise have been returned, so the async path no longer pays for an extra layer of indirection, Microsoft said.

In library improvements, System.Text.Json can now serialize and deserialize the new C# union types. The serializer recognizes a union through the new JsonTypeInfoKind.Union contract type, reads and writes the active case, and supports the reflection-based serializer and the source generator. The new JsonUnionAttribute, JsonUnionCaseInfo, and type-classifier APIs enable customization of how cases are discovered and named.

In C# improvements, extension members now include indexers, so developers can add this[...] access to a type from an extension block. This rounds out the extension members feature introduced in .NET 10, which already supported extension methods and properties. An extension indexer is declared like an instance indexer inside an extension block.

Other highlights in .NET 11 Preview 6:

  • Math.BigMul(long, long, out long) is now significantly faster on x64. The JIT generates a single MUL r/m64 instruction when both operands are 64-bit values and the caller requests the high half of the result. Previously the JIT emitted a helper call. The change eliminates the call overhead and makes code that multiplies large numbers tighter.
  • Minimal API validation in ASP.NET Core now supports asynchronous validators end-to-end.
  • For F#, the compiler emits better sequence points for several common debugging scenarios. Breakpoints and stepping are improved for call arguments that need stack-empty debug points, for expressions and comprehensions, simple literal bindings, and if or match conditions.
  • For .NET MAUI, .NET 11 Preview 6 advances the cross-platform CollectionView and Shell architecture, removes the legacy Xamarin.Forms compatibility package, and brings the Apple workloads onto stable Xcode 26.6.
  • Also for .NET MAUI, HybridWebView JavaScript interop is reworked to use a source generator instead of reflection, so it no longer produces trim or AOT (ahead of time) compiler warnings and works correctly in fully trimmed and Native AOT apps.
  • Projects that use Microsoft.TypeScript.MSBuild in Razor Class Libraries now properly integrate TypeScript compilation outputs with ASP.NET Core Static Web Assets.

GitHub Copilot introduces upgrade canvas for modernizing .NET applications

13 July 2026 at 17:27

GitHub Copilot has introduced an interactive canvas that makes it easier to follow, review, and steer the actions of the upgrade agent as it modernizes .NET applications.

The GitHub Copilot upgrade agent assesses the application, generates a structured upgrade plan, creates implementation tasks, and executes the work, according to a July 9 bulletin from Microsoft. Now developers can follow that workflow in the GitHub Copilot app through an interactive upgrade canvas. The upgrade canvas provides a live view of the modernization workflow as it unfolds, Microsoft said.

This agent upgrade agent starts by assessing the .NET application and identifying what needs to change:

  • What version of .NET is this application targeting?
  • Which NuGet packages need to be updated?
  • Are there breaking API changes?
  • Which projects can be upgraded independently?
  • What should happen first?

From this point, the agent generates a structured upgrade plan and breaks the work into actionable implementation tasks. While the GitHub Copilot app provides the interactive upgrade canvas, GitHub Copilot upgrade also is available in the following tools:

  • Visual Studio – Built directly into Visual Studio. Right-click a solution or project in Solution Explorer and select Modernize to start a .NET upgrade.
  • Visual Studio Code – Install the GitHub Copilot upgrade extension, select the Upgrade agent from the agent picker dropdown menu, and prompt the agent to modernize your .NET application.
  • GitHub Copilot CLI – Install the GitHub Copilot upgrade plugin to assess, plan, and execute .NET upgrades directly from the terminal.

Developers can get started with GitHub Copilot and the GitHub Copilot upgrade agent by visiting the GitHub Copilot upgrade marketplace.

❌