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

Sunday, March 2, 2025

ASP.NET Core: Configuring Authentication with Azure AD B2C using Explicit Configuration

In this post let's see how we can configure Authentication in an ASP.NET Core Web API with Azure AD B2C and most importantly using explicit configuration.

The standard approach is something like below.

services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(Configuration, "AzureAdB2C");

This I am highly against with, because we are picking up Configuration by the section named "AzureAdB2C" and it's not explicit. There are be many options that can be specified in section, and when we bound it like this, we don't know which are required and which are not.

So I personally always prefer being explicit over implicit.

public class AzureAdB2COptions
{
    /// <summary>
    /// Instance name: Ex: "https://{your-tenant-name}.b2clogin.com"
    /// </summary>
    /// <remarks>Required</remarks>
    public string Instance { getset}

    /// <summary>
    /// Domain name: Ex: "{your-tenant-name}.onmicrosoft.com"
    /// </summary>
    /// <remarks>Required</remarks>
    public string Domain { getset}

    /// <summary>
    /// Client Application Id
    /// </summary>
    /// <remarks>Required</remarks>
    public string ClientId { getset}

    /// <summary>
    /// Your Azure AD B2C Application Policy Id
    /// </summary>
    /// <remarks>Required</remarks>
    public string SignUpSignInPolicyId { getset}
}

And then, use the following overload.

AzureAdB2COptions azureAdB2COptions = 
    Configuration.GetSection("AzureAdB2C").Get<AzureAdB2COptions>();

services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(jwtBearerOptions =>
    {
        jwtBearerOptions.Audience = azureAdB2COptions.ClientId;
    },
    identityOptions =>
    {
        identityOptions.Instance = azureAdB2COptions.Instance;
        identityOptions.Domain = azureAdB2COptions.Domain;
        identityOptions.ClientId = azureAdB2COptions.ClientId;
        identityOptions.SignUpSignInPolicyId = azureAdB2COptions.SignUpSignInPolicyId;
    });

Hope this helps.

Happy Coding.

Regards,
Jaliya