Thursday, April 17, 2025

C# 14.0: Introducing Extension Members and Null-Conditional Assignment

Visual Studio 17.14.0 Preview 3.0 got released earlier today and now you can try out two nice C# 14.0 features that got released with .NET 10 Preview 3.0.

Extension Members


We all know about Extension Methods which was there since C# 3.0.
public static class INumberExtensions
{
    public static IEnumerable<int> WhereGreaterThan(this IEnumerable<int> sourceint threshold)
    {
        return source.Where(x => x > threshold);
    }

    public static bool AnyGreaterThan(this IEnumerable<int> sourceint threshold)
    {
        return source.WhereGreaterThan(threshold).Any();
    }
}
So here I have some extension methods for IEnumerable<int> which I can call like below:
IEnumerable<int> numbers = [1, 2, 3, 4, 5];

IEnumerable<int> largeNumbers = numbers.WhereGreaterThan(3);
bool hasLargeNumbers = numbers.AnyGreaterThan(3);
With C# 14.0, I can write the same with following syntax. Note: there is no use of this.
public static class INumberExtensions
{
    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> WhereGreaterThan(int threshold)
            => source.Where(x => x > threshold);

        public bool AnyGreaterThan(int threshold)
            => source.WhereGreaterThan(threshold).Any();
    }
}
It also supports generic types, something like following:
using System.Numerics;

public static class INumberExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : INumber<T>
    {
        public IEnumerable<T> WhereGreaterThan(T threshold)
            => source.Where(x => x > threshold);

        public bool AnyGreaterThan(T threshold)
            => source.WhereGreaterThan(threshold).Any();
    }
}

Null-Conditional Assignment


Consider the below class.
public class Person
{
    public required string Name { getinit}

    public DateOnly? Birthdate { getset}

    public string? Address { getset}
}
And say we need to have a method to update person details.
static void UpdatePerson(Person? personDateOnly birthdatestring address)
{
    if (person is null)
    {
        return;
    }

    person.Birthdate = birthdate;
    person.Address = address;
}
Here we need to explicitly check whether the person is null before assignment. Now with C# 14.0, we can do this:
static void UpdatePerson(Person personDateOnly birthdatestring address)
{
    person?.Birthdate = birthdate;
    person?.Address = address;
}
And this is exactly same as previous code, values will only get set if the person is not null.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 9, 2025

Azure DevOps: NuGet: Command Not Found with Ubuntu-Latest (24.04)

Our builds started failing today and it's Microsoft Servicing Tuesday even though it's Wednesday over here.

We are using ubuntu-latest in almost all our builds and started seeing the following error:

nuget: command not found

And our build pipelines was using NuGet.

task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

script: nuget restore
  displayName: NuGet Restore

And then saw the following warning:

##[warning] See https://aka.ms/azdo-ubuntu-24.04 for changes to the ubuntu-24.04 image. 
Some tools (e.g. Mono, NuGet, Terraform) are not available on the image. 
Therefore some tasks do not run or have reduced functionality.

Have been seen that warning, but didn't put much attention. The link explains the failure.

So it's time to use dotnet restore (too bad we are late to the party)

task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

task: DotNetCoreCLI@2
  displayName: .NET Restore
  inputs:
    command: 'custom'
    custom: 'restore'
    projects: '**/*.csproj'

And that's it, we are back to successful builds.

Look out for this one in your builds as well!

Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, April 7, 2025

Migrating Azure Durable Function App to Use Durable Task Scheduler: Running in Azure

In my previous post I wrote about Migrating Azure Durable Function App to Use Durable Task Scheduler: Running Locally. And in this post, let's see how to run an Azure Durable Function App with DTS in Azure.

As of today DTS supports function apps that uses App Service Plan or Functions Premium plan. And also it's not supported in all the regions. You can run the following command to check the supported regions.

az provider show `
    --namespace Microsoft.DurableTask `
    --query "resourceTypes[?resourceType=='schedulers'].locations | [0]" `
    --out table
DTS: Supported Regions
So I have an Azure Durable Function App, the same application which is mentioned in the above post (except the changes done to use DTS). It uses an App Service Plan and is on East US 2. So we are all good to start migrating the Azure Durable Function App to use DTS.

Assuming you have az cli installed and logged in, let's add durabletask extension.
az extension add --name durabletask

# If you have it installed already, upgrade it to the latest version
# az extension add --upgrade --name durabletask
Now, let's create the scheduler,
az durabletask scheduler create `
    --name "<SCHEDULER_NAME>" `
    --resource-group "<RESOURCE_GROUP_NAME>" `
    --location "<REGION>" `
    --ip-allowlist "[0.0.0.0/0]" `
    --sku-name "dedicated" `
    --sku-capacity "1"
This is going to take some time to complete.
az durabletask scheduler create
Make note of the endpoint. Now let's create a taskhub:
az durabletask taskhub create `
    --name "default" `
    --resource-group "<REESOURCE_GROUP_NAME>" `
    --scheduler-name "<SCHEDULER_NAME>"
az durabletask taskhub create
You can also do this in Azure Portal by searching for Durable Task Scheduler and create.
Durable Task Scheduler
Durable Task Scheduler
Now the Scheduler and the TaskHub is created, next we need to grant our function app access to this Scheduler and/or TaskHub. DTS only supports either user-assigned or system-assigned managed identity authentication.

So let's do the following. 
  1. Create an user-assigned managed identity.
  2. Assign a role to managed identity. It can be one of the following:
    1. Durable Task Data Contributor: Role for all data access operations. This role is a superset of all other roles.
    2. Durable Task Worker: Role used by worker applications to interact with the durable task scheduler. Assign this role if your app is used only for processing orchestrations, activities, and entities.
    3. Durable Task Data Reader: Role to read all durable task scheduler data. Assign this role if you only need a list of orchestrations and entities payloads.
  3. Assign the identity to the function app.
# 1. Create an user-assigned managed identity
az identity create `
    --resource-group "<RESOURCE_GROUP_NAME>" `
    --name "mi-func-dts"

## get the identity id
$managedIdentityClientId = az identity show `
    --resource-group "<RESOURCE_GROUP_NAME>" `
    --name "mi-func-dts" `
    --query clientId `
    --output tsv

# 2. Assign a role to managed identity

## Scope can be either to the entire scheduler or to the specific task hub

### scheduler
#scope = "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>/providers/Microsoft.DurableTask/schedulers/<SCHEDULER_NAME>"

### task hub
$scope = "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>/providers/Microsoft.DurableTask/schedulers/<SCHEDULER_NAME>/taskHubs/<TASKHUB_NAME>"

az role assignment create `
  --assignee "$managedIdentityClientId" `
  --role "Durable Task Data Contributor" `
  --scope "$scope"

# 3. Assign the identity to the function app
$managedIdentityResourceId = az resource show `
    --resource-group "<RESOURCE_GROUP_NAME>" `
    --name "mi-func-dts" `
    --resource-type Microsoft.ManagedIdentity/userAssignedIdentities `
    --query id `
    --output tsv

az functionapp identity assign `
    --resource-group "<RESOURCE_GROUP_NAME" `
    --name "<FUNCTION_APP_NAME>" 
    --identities "$managedIdentityResourceId"
Now we are almost done.

Final step is deploying new code and updating app settings. Code changes (Migrating Azure Durable Function App to Use Durable Task Scheduler: Running Locally) are deployed and app settings are updated as follows:
{
  "name""DTS_CONNECTION_STRING",
  "value""Endpoint=<SCHEDULER_ENDPOINT>;Authentication=ManagedIdentity;ClientID=<MANAGED_IDENTITY_CLIENTID>",
  "slotSetting"false
},
{
  "name""TASKHUB_NAME",
  "value""default",
  "slotSetting"false
}
Now let's hit the endpoint and make sure it's working.
Test the function app
Wonderful.

Now we want to look at the DTS Dashboard. For that, let's grant our Azure account access to DTS.
# Get the current user id
$assignee = az ad signed-in-user show `
    --query id `
    --output tsv

# Set the scope (can be either scheduler or task hub level), I am giving the user scheduler level
$scope = "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>/providers/Microsoft.DurableTask/schedulers/<SCHEDULER_NAME>"

# Assign the role to the user
az role assignment create `
  --assignee "$assignee" `
  --role "Durable Task Data Contributor" `
  --scope "$scope"
Now go to: https://dashboard.durabletask.io/, and fill out the details required.

And there it is.
DTS Dashboard
DTS Dashboard: Orchestration Detail

Happy Coding.

Regards,
Jaliya

Thursday, April 3, 2025

Migrating Azure Durable Function App to Use Durable Task Scheduler: Running Locally

Durable Task Scheduler (DTS) is the latest addition to Azure Durable Functions. The public preview is announced a couple of weeks ago, and you can find it here: Announcing the public preview launch of Azure Functions durable task scheduler. Also if you are using Azure Durable Functions with Netherite,  Netherite will no longer receive official support after 31 March 2028. The replacement is DTS, so it's time to get into the game.

In this post I am not going to be writing what DTS is, above post is very detailed. Instead here in this post, let's see how we can migrate an existing durable function app to use DTS as the backend.

I have the following .NET 9.0 Isolated simple durable function app. Here I also have a simple Durable Entity as I want to make sure DTS supports Durable Entities as well.
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Entities;

namespace FunctionsPlayground;

public static class Function
{
    [Function(nameof(HttpStart))]
    public static async Task<HttpResponseData> HttpStart(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
        [DurableClient] DurableTaskClient client,
        FunctionContext executionContext)
    {
        string instanceId =  await client.ScheduleNewOrchestrationInstanceAsync(nameof(RunOrchestrator));

        return await client.CreateCheckStatusResponseAsync(reqinstanceId);
    }

    [Function(nameof(RunOrchestrator))]
    public static async Task<List<string>> RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        EntityInstanceId entityId = new(nameof(HelloHistoryEntity)"helloHistory");

        await context.Entities.CallEntityAsync<string>(entityId nameof(HelloHistoryEntity.Reset));

        string result = await context.CallActivityAsync<string>(nameof(SayHello)"Tokyo");
        await context.Entities.CallEntityAsync<string>(entityId nameof(HelloHistoryEntity.Add) result);

        result = await context.CallActivityAsync<string>(nameof(SayHello)"Seattle");
        await context.Entities.CallEntityAsync<string>(entityId nameof(HelloHistoryEntity.Add) result);

        result = await context.CallActivityAsync<string>(nameof(SayHello)"London");
        await context.Entities.CallEntityAsync<string>(entityId nameof(HelloHistoryEntity.Add) result);

        List<string> outputs = await context.Entities.CallEntityAsync<List<string>>(entityId nameof(HelloHistoryEntity.Get));
        return outputs;
    }

    [Function(nameof(SayHello))]
    public static string SayHello([ActivityTrigger] string nameFunctionContext executionContext)
    {
        return $"Hello {name}!";
    }
}

public class HelloHistoryEntity : TaskEntity<List<string>>
{
    public void Add(string message) => State.Add(message);

    public void Reset() => State = [];

    public List<string> Get() => State;

    [Function(nameof(HelloHistoryEntity))]
    public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
    {
        return dispatcher.DispatchAsync(this);
    }
}
Now this is running fine and I want to change the backend to use DTS.

First step is setting up DTS emulator locally. For that, I am running the following docker commands. Note: Docker is a prerequisite.
docker pull mcr.microsoft.com/dts/dts-emulator:v0.0.5
docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:v0.0.5
DTS Emulator Running Locally
Here:
  • 8080: gRPC endpoint that allows an app to connect to the scheduler
  • 8082: Endpoint for durable task scheduler dashboard
I can navigate to http://localhost:8082/ in the browser and ensure it's all good.
DTS Dashboard
Next step is updating the project.

First I am adding the following package to the project.
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged --prerelease
Then updating the host.json configuring the durableTask extension.
{
  "version""2.0",
  "extensions": {
    "durableTask": {
      "hubName""%TASKHUB_NAME%",
      "storageProvider": {
        "type""azureManaged",
        "connectionStringName""DTS_CONNECTION_STRING"
      }
    }
  }
}
Now finally setting the appsettings for DTS in local.settings.json.
{
  "IsEncrypted"false,
  "Values": {
    "AzureWebJobsStorage""UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME""dotnet-isolated",
    "DTS_CONNECTION_STRING""Endpoint=http://localhost:8080;Authentication=None",
    "TASKHUB_NAME""default"
  }
}
Now I am locally all set to test the durable function app backed by DTS.

I have executed the HTTP function, and checked the DTS dashboard:
DTS Dashboard
DTS Dashboard: Orchestration Detail
Look at that, this is amazing. 



Hope this helps and do start evaluating DTS. If you find any issues: please do report it here: https://github.com/Azure/Durable-Task-Scheduler

Happy Coding.

Regards,
Jaliya

Wednesday, March 26, 2025

Azure DevOps Classic Release Pipelines: Deploying .NET 9 Isolated Azure Function App on Linux

If you are using Azure DevOps classic releases, you might have noticed, that we still don't have Runtime stack support for DOTNET-ISOLATED|9.0 in Azure Functions Deploy task.
Azure Functions Deploy
So how can we use this to deploy a .NET 9 Isolated Azure Function App on Linux.

It's quite easy, you can just type in DOTNET-ISOLATED|9.0 as the Runtime stack, and upon deployment the correct .NET Version will get set.
Azure Function App on Linux: .NET Version
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, March 19, 2025

Azure Functions Isolated: SQL Trigger

In this post, let's see how we can use an Isolated Azure Function to monitor a SQL table for changes.

Let's start by creating an new Azure Functions project. I am choosing a Function template as SQL trigger. 
Isolated Azure Function: SQL trigger
Once the project is created, it's going to look more or less like the below (I changed the default ToDoItem POCO class to match my table)
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.Sql;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace FunctionApp1;

public class Function1
{
    private readonly ILogger _logger;

    public Function1(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<Function1>();
    }

    [Function("Function1")]
    public void Run(
        [SqlTrigger("[dbo].[Users]""SqlConnectionString")] IReadOnlyList<SqlChange<User>> changes,
            FunctionContext context)
    {
        _logger.LogInformation("SQL Changes: {ChangeSet}",
            JsonSerializer.Serialize(changesnew JsonSerializerOptions
            {
                WriteIndented = true,
                Converters =
                {
                    new JsonStringEnumConverter()
                }
            }));
    }
}

public class User
{
    public int Id { getset}

    public string FirstName { getset}

    public string LastName { getset}
}
The SQL trigger uses Change Tracking (SQL Server) functionality to monitor a SQL table for changes. And once it detects a change which can be either when a row created, updated, or deleted, the function is triggered.

So before running the project, we need to enable Change Tracking in our targeted table in the database.
-- 1. enable change tracking for the database
ALTER DATABASE ['<DatabaseName>']
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);
 
-- 2. enable change tracking for the table
ALTER TABLE <TableName>
ENABLE CHANGE_TRACKING;

---- disable change tracking for the table
--ALTER TABLE <TableName>
--DISABLE CHANGE_TRACKING;

---- disable change tracking for the database
--ALTER DATABASE ['<DatabaseName>']
--SET CHANGE_TRACKING = OFF  
And now since we are going to be doing a test run of this function locally first, we need to add a special environment variable: WEBSITE_SITE_NAME to local.settings.json.
{
  "IsEncrypted"false,
  "Values": {
    "AzureWebJobsStorage""UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME""dotnet-isolated",
    "SqlConnectionString""<SqlConnectionString>",
    "WEBSITE_SITE_NAME""func-sqltriggerdemo-local"
  }
}
And now we can run Azure Function project and let's make a change to the data in the table.
SQL trigger
More read:
   Azure SQL trigger for Functions

Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, March 13, 2025

C# 14.0: Introducing the field keyword

In this post, let's have a look at a nice feature that is coming with C# 14.0

Currently it's still on preview, so you can try this feature out with .NET 9.0 (C# 14.0 is supported on .NET 10 though) or .NET 10 .0 with LangVersion set to preview.

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <LangVersion>preview</LangVersion> ...
</PropertyGroup>

or

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <LangVersion>preview</LangVersion>
  ...
</PropertyGroup>

Now let's have a look.

Consider the following class.

public class Employee
{
    // Other properties

    public required DateOnly Birthday { getset}
}

Say you have a requirement, where Employees Age needs to be >= 21. We can introduce that validation when setting a value for the Birthday, but in order to do that, and just for that, we need to add a backing field for Birthday, something like below:

public class Employee
{
    private const int MinAge = 21;

    // Other properties

    private DateOnly _birthday;

    public required DateOnly Birthday
    {
        get => _birthday;
        set
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(value DateOnly.FromDateTime(DateTime.Now.AddYears(-MinAge)));

            _birthday = value;
        }
    }
}

But what if we can do the same without using the backing field? C# 14.0 introduces a new contextual keyword field.

public class Employee
{
    private const int MinAge = 21;

    // Other properties

    public required DateOnly Birthday
    {
        get;
        set
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(value DateOnly.FromDateTime(DateTime.Now.AddYears(-MinAge)));

            field = value;
        }
    }
}

So here just as a value contextual keyword, we now have a field contextual keyword. We no longer need the backing field, therefore we also don't need to provide a body for the get accessor.

I am loving it.

Keep watching this space for more C# 14.0 features:
   What's new in C# 14

Happy Coding.

Regards,
Jaliya