Tuesday, May 26, 2026

Microsoft Agent Framework: Deterministic Multi-Agent Orchestrations with .NET Durable Functions

In this post, let's have a look at building deterministic multi-agent orchestrations with Microsoft Agent Framework in .NET using Durable Functions to coordinate multiple agents.

A couple of earlier posts set the stage, if you're jumping in here, those are worth reading first:
We've got an Azure Functions app hosting Microsoft Agent Framework agents, backed by Durable Functions on Durable Task Scheduler (DTS). Now we want to coordinate multiple agents in a controlled way.

Why a Durable Functions orchestrator?

A popular pattern for multi-agent systems is to let one agent route between other agents, the "agent-as-orchestrator" pattern. That is great for flexibility, but the routing decisions are non-deterministic, the same input can take different paths on different runs, which makes auditing, retries, and replay harder.

When we want deterministic behavior, same input, same path, with retries and replay handled by the platform, we can put the orchestration logic in a Durable Functions orchestrator. The agents stay the "smart" bits; the orchestrator is plain C# code that decides what runs when, fully replayable.

The example

We'll build on the weekend planner from the previous posts, with a twist:
  • A weather-assessor agent decides whether the weather is suitable for outdoor activities.
  • If yes, an itinerary-planner agent builds a full weekend itinerary.
  • If no, we short-circuit and call an activity to suggest indoor options instead.
Two agents, one orchestrator, deterministic branching.

Naming the agents

Since the orchestrator resolves agents by name, let's keep them in a small constants class to avoid magic strings:
internal static class Agents
{
    public const string WeatherAssessor = "weather-assessor";

    public const string ItineraryPlanner = "itinerary-planner";
}
Registering the agents

Same shape as the previous post, just two agents this time. Each has its own instructions and tools (tools code omitted for brevity, same weather/activities/date functions as before):
using Azure.AI.Projects;
using Azure.Identity;
using MafSamples.DurableOrchestrator.Constants;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;

var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!;

// For local development, using AzureCliCredential
var credential = new AzureCliCredential();
var projectClient = new AIProjectClient(new Uri(endpoint), credential);

// Agent 1 — assesses whether the weather is suitable for outdoor activities
AIAgent weatherAssessor = projectClient
    .AsAIAgent(
        model: deploymentName,
        name: Agents.WeatherAssessor,
        instructions: """
            You determine whether the weather in a given city is suitable for outdoor leisure activities this weekend.
            Use the available tools to check the weather and current date.
            Respond with a structured result: IsSuitableForOutdoors and a short Reason.
            """,
        tools: [
            AIFunctionFactory.Create(Tools.GetWeather),
            AIFunctionFactory.Create(Tools.GetCurrentDate)
        ]);

// Agent 2 — builds the weekend itinerary
AIAgent itineraryPlanner = projectClient
    .AsAIAgent(
        model: deploymentName,
        name: Agents.ItineraryPlanner,
        instructions: """
            You build a weekend itinerary using the available activities for a given city.
            Include the date of the weekend in the itinerary.
            Respond with a structured result containing a Summary string.
            """,
        tools: [
            AIFunctionFactory.Create(Tools.GetActivities),
            AIFunctionFactory.Create(Tools.GetCurrentDate)
        ]);

// Register the agents so orchestrations can resolve them via context.GetAgent(name)
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableAgents(options =>
    {
        options.AddAIAgent(weatherAssessor);
        options.AddAIAgent(itineraryPlanner);
    })
    .Build();

app.Run();


The orchestrator

This is the new piece. Each agent is resolved from the orchestration context, and we drive the flow with regular C#, including the if/else branch:
using MafSamples.DurableOrchestrator.Constants;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;

public static class WeekendPlanOrchestrations
{
    [Function(nameof(WeekendPlanOrchestration))]
    public static async Task<string> WeekendPlanOrchestration(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        WeekendRequest request = context.GetInput<WeekendRequest>()!;

        // 1. Ask the weather-assessor whether the weather is suitable for outdoor activities
        DurableAIAgent weatherAgent = context.GetAgent(Agents.WeatherAssessor);
        AgentSession weatherSession = await weatherAgent.CreateSessionAsync();

        AgentResponse<WeatherAssessment> weatherResponse = await weatherAgent.RunAsync<WeatherAssessment>(
            message: $"Is the weather in {request.City} suitable for outdoor activities this weekend?",
            session: weatherSession);

        WeatherAssessment assessment = weatherResponse.Result;

        if (!assessment.IsSuitableForOutdoors)
        {
            return await context.CallActivityAsync<string>(nameof(NotifyIndoorPlan), assessment.Reason);
        }

        // 2. Ask the itinerary-planner to build a plan
        DurableAIAgent plannerAgent = context.GetAgent(Agents.ItineraryPlanner);
        AgentSession plannerSession = await plannerAgent.CreateSessionAsync();

        AgentResponse<WeekendItinerary> plannerResponse = await plannerAgent.RunAsync<WeekendItinerary>(
            message: $"Create a weekend itinerary for {request.City}",
            session: plannerSession);

        return plannerResponse.Result.Summary;
    }

    [Function(nameof(NotifyIndoorPlan))]
    public static string NotifyIndoorPlan([ActivityTrigger] string reason)
    {
        Console.WriteLine($"[Activity] Suggesting indoor plan. Reason: {reason}");
        return $"Bad weather for outdoor activities ({reason}). Try indoor options like museums or movies.";
    }
}

// Structured outputs requested from the two agents inside the orchestration
public record WeatherAssessment(bool IsSuitableForOutdoors, string Reason);

public record WeekendItinerary(string Summary);
A few things worth pointing out:
  • context.GetAgent(name) resolves a DurableAIAgent: an agent that runs inside the orchestration with its state persisted durably.
  • CreateSessionAsync starts a fresh conversation session for this orchestration run.
  • RunAsync<T> sends a message and gets a structured response back, typed to our record (WeatherAssessment, WeekendItinerary).
  • The if branch is deterministic: on replay, the same WeatherAssessment will always lead to the same path.
  • CallActivityAsync on the "bad weather" branch shows that we can still mix in regular Durable activities alongside agent calls.
Kicking it off

An HTTP starter accepts a city and schedules a new orchestration instance:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using System.Net;

public static class WeekendPlanStarter
{
    [Function(nameof(StartWeekendPlan))]
    public static async Task<HttpResponseData> StartWeekendPlan(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "weekend-plan")] HttpRequestData req,
        [DurableClient] DurableTaskClient client)
    {
        WeekendRequest? request = await req.ReadFromJsonAsync<WeekendRequest>();
        if (request is null || string.IsNullOrWhiteSpace(request.City))
        {
            HttpResponseData badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
            await badRequest.WriteStringAsync("Body must include a non-empty 'city'.");
            return badRequest;
        }

        string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
            nameof(WeekendPlanOrchestrations.WeekendPlanOrchestration), request);

        return await client.CreateCheckStatusResponseAsync(req, instanceId);
    }
}

// API contract for the HTTP starter
public record WeekendRequest(string City);
Send a request and we're off:
POST http://localhost:7100/api/weekend-plan
Content-Type: application/json

{
    "city": "Auckland"
}
  
We will get a response with 202 Accepted with Orchestration response.

Now we get to see the real benefit of running on DTS. We can open up DTS Dashboard and the Orchestrations tab shows the orchestration instance and its full history, every agent call, every activity, replays and all:
DTS: Orchestrations
All the AI happens inside individual agents; all the "what runs when" lives in code we can read, test, and reason about. That trade-off, less agent autonomy in exchange for more predictability is often exactly what you want in a production workflow.

Complete source code:

Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, May 25, 2026

Microsoft Agent Framework: Agents on Azure Functions with Durable Task Scheduler

In the previous post: Microsoft Agent Framework: Agents on Azure Functions with .NET, we had a look at hosting an Agent on Azure Functions using Microsoft Agent Framework, backed by Azurite for local Durable Functions state. In this post, let's swap that backend for Durable Task Scheduler (DTS) and see what it brings.

If you haven't read my earlier posts on DTS, those cover the backend itself in detail, well worth reading first:
In short: DTS is a managed orchestration backend for Durable Functions. Compared to the default Azure Storage backend, we get a managed task hub, better throughput, a built-in dashboard for inspecting orchestrations and their history, and new and especially relevant for us  an Agents tab for inspecting agent threads directly.

The only changes

The agent code from the previous post doesn't change at all. The HTTP endpoint, the tools, all stay exactly the same. We only touch three files (versions are the latest as of today, they will change):

Add the DTS extension package to the .csproj:
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.8.1" />
Point the durableTask extension at DTS in host.json:
{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "hubName": "%TASKHUB_NAME%",
      "storageProvider": {
        "type": "azureManaged",
        "connectionStringName": "DTS_CONNECTION_STRING"
      }
    }
  }
}
And add the two values in local.settings.json:
"DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None",
"TASKHUB_NAME": "default",
For local development, I'm running the DTS emulator via Docker on port 8080, setting that up is covered in the first DTS post linked above.

What we get

Run the Functions host and fire the same HTTP request from the previous post:
POST http://localhost:7099/api/agents/weekend-planner/run
Content-Type: application/json

{
  "message": "What should I do this weekend in Auckland?"
}
  
The response and behavior is exactly what we had before. 

But now, when we open the DTS emulator dashboard, we can see the something like following.
Entities
In the Entities tab, behind the scenes, the framework stores each agent thread as a Durable Entity, one entity per session, so the conversation state survives restarts and can be picked up on the next turn. We don't write any of this code ourselves; it's how the hosting package persists state on top of DTS:
Agents
The more interesting view for us is the new Agents tab (currently in preview). It lists each agent session on its own row, so the same agent will appear multiple times if it has handled multiple conversations, one row per session. From here we can jump into any session:
Agents Detail
Under Agents is where it really pays off. The dashboard shows the token usage for the whole conversation, a Timeline with each prompt and response, and the tool calls the agent made for each exchange, GetCurrentDate, GetWeather, GetActivities and each marked done. There's also a Chat History tab if we prefer the linear message view. For debugging agent flows, this is exactly the kind of view we'd previously have had to build ourselves.

This is one of the nicer things about the Functions hosting story for Microsoft Agent Framework, because agent conversations are modeled as Durable Functions orchestrations, anything that improves the Durable stack (like DTS) improves the agent experience for free.


Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, May 19, 2026

Microsoft Agent Framework: Agents on Azure Functions with .NET

In this post, let's have a look at hosting an Agent on Azure Functions using the Microsoft Agent Framework. In a previous post, I have have written about getting started with Microsoft Agent Framework in .NET. If you haven't read the previous post, I would recommend doing so.

In the previous post, we have been running our agent as a console application. That's great for learning, but it isn't how we would actually run agents in production. Agents typically need an HTTP endpoint to be invoked from a client, and they need to persist conversation state across requests so a user can have a multi-turn conversation.

The Microsoft Agent Framework ships with a hosting package for Azure Functions (Microsoft.Agents.AI.Hosting.AzureFunctions) that gives us both of these out of the box. It builds on top of Durable Functions, so each conversation thread is durably persisted, can survive process restarts, and can be resumed later. Let's see how it works.

Let's start creating an Azure Functions worker project targeting .NET 10 (isolated worker model). On top of the usual Functions packages, we need a couple of extras (versions are the latest as of today, they will change):
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput>
    <RootNamespace>MafSamples.DurableFunctions</RootNamespace>
    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
    <PackageReference Include="Azure.Identity" Version="1.21.0" />
    <PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
    <PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" Version="1.6.1-preview.260514.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>

</Project> 
Microsoft.Agents.AI.Hosting.AzureFunctions does the heavy lifting and plugs the agent into the Functions host, exposes an HTTP endpoint per agent, and wires up Durable Functions backed state for each conversation thread.

local.settings.json

Durable Functions needs a storage backend, and for local development we will be using Azurite storage emulator for simplicity. We could also go with the Durable Task Scheduler (DTS) and get its monitoring features out of the box, but let's leave that for another post.
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AZURE_AI_ENDPOINT": "https://<your-foundry-resource>.services.ai.azure.com/",
    "AZURE_AI_MODEL": "<DEPLOYMENT_NAME>"
  }
}

Program.cs

Here is the entire program. The agent definition is the same one we have used in the previous post, a small weekend-planner with three tools. The new changes are FunctionsApplication.CreateBuilder and ConfigureDurableAgents:
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.DurableTask;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Hosting;
using System.ComponentModel;

var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!;

// For local development, using AzureCliCredential
var credential = new AzureCliCredential();

AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
    .AsAIAgent(
        model: deploymentName,
        name: "weekend-planner",
        instructions: """
            You help users plan their weekends and choose the best activities for the given weather.
            If an activity would be unpleasant in weather, don't suggest it.
            Include date of the weekend in response.
            """,
        tools: [
            AIFunctionFactory.Create(GetWeather),
            AIFunctionFactory.Create(GetActivities),
            AIFunctionFactory.Create(GetCurrentDate)
        ]);

// Automatically creates HTTP endpoints and manages state persistence
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableAgents(options =>
        options.AddAIAgent(agent)
    )
    .Build();

app.Run();

[Description("Returns weather data for a given city.")]
static WeatherResult GetWeather(
    [Description("The city to get the weather for.")] string city)
{
    Console.WriteLine($"[Tool] Getting weather for '{city}'.");

    return new WeatherResult(18, "Rainy");
}

[Description("Returns a list of leisure activities for a given city and date, each with a name and location.")]
static List<LeisureActivity> GetActivities(
    [Description("The city to get activities for.")] string city,
    [Description("The date to get activities for in format YYYY-MM-DD.")] string date)
{
    Console.WriteLine($"[Tool] Getting activities for '{city}' on '{date}'.");

    return
    [
        new("Hiking", city),
        new("Beach", city),
        new("Museum", city)
    ];
}

[Description("Gets the current date from the system and returns as a string in format YYYY-MM-DD.")]
static string GetCurrentDate()
{
    Console.WriteLine("[Tool] Getting current date");

    return DateTime.Now.ToString("yyyy-MM-dd");
}

record WeatherResult(int Temperature, string Description);

record LeisureActivity(string Name, string Location);

// Dummy orchestrator to satisfy Durable Functions requirement of having at least one orchestration in the assembly.
public static class DummyOrchestrator
{
    [Function(nameof(DummyOrchestrator))]
    public static Task RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        return Task.CompletedTask;
    }
}
Notice we don't define any HTTP-triggered functions ourselves. ConfigureDurableAgents registers an HTTP endpoint for every agent we add via AddAIAgent, using the agent's name. For our weekend-planner agent, the endpoint will be:
POST /api/agents/weekend-planner/run  
The one quirky bit is the DummyOrchestrator. The Functions runtime needs at least one [OrchestrationTrigger] to be discovered in the assembly for the Durable extension to bind correctly. (see issue: microsoft/agent-framework/issues/5927)

Now let's run the Azure Function App and we can see something like below:
Function App
Then we can call the agent over HTTP. Here is a request to start a new conversation:
POST http://localhost:7098/api/agents/weekend-planner/run
Content-Type: application/json

{
  "message": "What should I do this weekend in Auckland?"
}  
The response comes back with the agent's reply.
Start a chat
Note the x-ms-thread-id under response Headers. That value is the durable identifier for the conversation. To continue the same conversation, we pass it back as a query parameter:
POST http://localhost:7098/api/agents/weekend-planner/run?thread_id=<x-ms-thread-id>
Content-Type: application/json

{
  "message": "What is the weather like there?"
}
  
Chat Continuation
The agent remembers we were talking about a specific city and answers in context without us writing a single line of state management code. Without a thread, if we ask the same above question as a fresh conversation, it will return something like What is the city etc. The thread, messages, and tool calls are all persisted in the storage account via Durable Functions. If the Functions host restarts, the conversation continues from where it left off.


Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, May 13, 2026

EF Core 11.0: Temporal Period Properties Backed by CLR Properties

In this post, let's have a look at a small but very welcome improvement to SQL Server Temporal Tables support in EF Core shipping with .NET 11 Preview 4, the ability to map temporal period columns (PeriodStart and PeriodEnd) to regular CLR properties on the entity.

Long time ago I blogged about EF Core 6.0: Introducing Support for SQL Server Temporal Tables, where we saw how to opt an entity into temporal tracking using IsTemporal(). Back then, the period columns were always managed as shadow properties, which meant we had to read them through EF.Property<DateTime>(...) whenever we wanted to project, filter, or order by them. That works, but it isn't great when you actually want to surface those values on the entity itself.

Starting with EF Core 11 Preview 4, we now have new strongly-typed overloads of HasPeriodStart and HasPeriodEnd that accept a lambda expression pointing to a CLR property on the entity. EF Core still wires them up with ValueGenerated.OnAddOrUpdate and BeforeSaveBehavior.Ignore under the hood, so SQL Server's SYSTEM_TIME machinery keeps owning the values, we just get to read them like any other column.

Let's have a look at it in action.

First, the project file. Make sure you are using >= EF Core 11 Preview 4.
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="11.0.0-preview.4.26230.115">
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  <PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="11.0.0-preview.4.26230.115" />
Now the entity. Notice how PeriodStart and PeriodEnd are just plain DateTime properties, no shadow property tricks required.
public class Category
{
    public int Id { get; set; }

    public string Name { get; set; }

    public DateTime PeriodStart { get; set; }

    public DateTime PeriodEnd { get; set; }
}
And here is the DbContext. The interesting bit is inside IsTemporal, where we now pass a property selector to HasPeriodStart and HasPeriodEnd.
public class MyDbContext : DbContext
{
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"<ConnectionString>");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>()
            .ToTable(c => c.IsTemporal(t =>
            {
                t.HasPeriodStart(x => x.PeriodStart);
                t.HasPeriodEnd(x => x.PeriodEnd);
            }));
    }
}
Now let's add a row, update it, and read the history back. Note that the OrderBy and the projection just use x.PeriodStart and x.PeriodEnd directly, no more EF.Property<DateTime>(x, "PeriodStart").
using var context = new MyDbContext();

context.Database.EnsureDeleted();
context.Database.EnsureCreated();

Category category = new() { Name = "Category A" };
await context.Categories.AddAsync(category);
await context.SaveChangesAsync();

category.Name = "Category A Updated";
context.Categories.Update(category);
await context.SaveChangesAsync();

foreach (var item in await context.Categories
    .TemporalAll()
    .Where(x => x.Id == category.Id)
    .OrderBy(x => x.PeriodStart)
    .Select(x => new
    {
        Category = x,
        x.PeriodStart,
        x.PeriodEnd
    })
    .ToListAsync())
{
    Console.WriteLine($"Name: '{item.Category.Name}', " + $"Start: '{item.PeriodStart}' - End: '{item.PeriodEnd}'.");
}
And the output:
Name: 'Category A', Start: '13/05/2026 9:53:01 am' - End: '13/05/2026 9:53:01 am'.
Name: 'Category A Updated', Start: '13/05/2026 9:53:01 am' - End: '31/12/9999 11:59:59 pm'.
A couple of things worth calling out:
  • EF Core still owns the values. The CLR properties are configured to be ignored on save, so even if you set them in code, SQL Server's SYSTEM_TIME will overwrite them on insert/update.
  • The current row will always have PeriodEnd set to 31/12/9999 11:59:59 pm, that is SQL Server's sentinel for "still active".
  • All the existing temporal query operators: TemporalAll, TemporalAsOf, TemporalFromTo, TemporalBetween, TemporalContainedIn, keep working exactly as before.
Small change, but a really nice quality-of-life improvement for anyone working with temporal tables.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 29, 2026

EF Core 11.0: Querying JSON Columns with JsonPathExists and JsonContains

In this post, let's have a look at two new functions introduced in EF Core 11.0EF.Functions.JsonPathExists() and EF.Functions.JsonContains(). Both are aimed at making it easier to query JSON data stored in your database, without falling back to raw SQL.

For SQL Server, JsonPathExists() translates to the JSON_PATH_EXISTS function (available since SQL Server 2022), and JsonContains() translates to the JSON_CONTAINS function (which is new in SQL Server 2025).

At the time of writing, .NET 11 Preview 3 is the latest and we need to be using EF Core 11 Preview 3 NuGet packages for these functions to work.
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="11.0.0-preview.3.26207.106">
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  <PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="11.0.0-preview.3.26207.106" />
Let's set up a simple Customer with a Contact as a complex type stored as a JSON column. The Contact  wraps an Address. Note that JSON_CONTAINS requires SQL Server 2025, so we are also setting the compatibility level to 170 via UseCompatibilityLevel(170). Without that, EF Core won't translate JsonContains() for you.
public class Customer
{
    public int Id { get; set; }

    public required string Name { get; set; }

    public required Contact Contact { get; set; }
}

public class Contact
{
    public required Address Address { get; set; }
}

public class Address
{
    public required string Street { get; set; }

    public required string City { get; set; }

    public required string State { get; set; }

    public required string PostalCode { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"<ConnectionString>", o => o.UseCompatibilityLevel(170));
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>()
            .ComplexProperty(c => c.Contact, b => b.ToJson());
    }
}

EF.Functions.JsonPathExists()


This checks whether a given JSON path exists inside a JSON document. The nice thing is, since our Contact is a complex type mapped to JSON, we can pass the property directly and use a typed-style path against the underlying JSON shape. Say we want to defensively pull customers whose Contact JSON actually has an Address.PostalCode:
// EF.Functions.JsonPathExists(): customers whose Contact JSON has an Address.PostalCode
List<Customer> customersWithPostalCode = await context.Customers
    .Where(c => EF.Functions.JsonPathExists(c.Contact, "$.Address.PostalCode"))
    .ToListAsync();
And the SQL generated:
SELECT [c].[Id], [c].[Name], [c].[Contact]
FROM [Customers] AS [c]
WHERE JSON_PATH_EXISTS([c].[Contact], N'$.Address.PostalCode') = 1

EF.Functions.JsonContains()


While EF Core 11 already automatically translates LINQ Contains queries over primitive collections to JSON_CONTAINS (when targeting SQL Server 2025), there are cases where you want to invoke it directly, for example, to search for a value at a specific JSON path. That's where this function comes in.

One small heads-up: at the time of writing, JsonContains() is flagged as experimental (diagnostic EF9106), so you'll need to suppress it at the call site for the project to build.

Let's pull all the customers in Redmond:
// EF.Functions.JsonContains(): customers in Redmond
#pragma warning disable EF9106 // JsonContains is for evaluation purposes only
List<Customer> redmondCustomers = await context.Customers
    .Where(c => EF.Functions.JsonContains(c.Contact, "Redmond", "$.Address.City") == 1)
    .ToListAsync();
#pragma warning restore EF9106
And the generated SQL:
SELECT [c].[Id], [c].[Name], [c].[Contact]
FROM [Customers] AS [c]
WHERE JSON_CONTAINS([c].[Contact], N'Redmond', N'$.Address.City') = 1
Both functions can be used with scalar string properties, complex types, and owned entity types mapped to JSON columns, so as you can see, you don't need a raw JSON string column to take advantage of these. And as a bonus, JSON_CONTAINS can take advantage of a JSON index, if one is defined on the column.

Read more:

Hope this helps.

Happy Coding.

Regards,
Jaliya