Monday, February 17, 2025

Azure DevOps Classic Release Pipelines: Read Variables in a Variable Group and Update Azure App Service AppSettings

In this post let's see how to read variables in a Variable Group and deploy them to Azure App Service as app settings from a Classic Azure DevOps Release Pipeline.

In the release pipeline I have Azure CLI task added and the release pipeline is running on Azure Hosted windows-latest agent.

Release Pipeline
In the Azure CLI task, I am doing the following.

Acquire a PAT (Personal Access Token) and set it.

$PAT = "<YOUR_PAT>"
$env:AZURE_DEVOPS_EXT_PAT = $PAT

Now set the default organization and project for az devops command.

az devops configure --defaults `
    organization=https://dev.azure.com/<YOUR_ORGANIZATION>/ `
    project=<YOUR_PROJECT>

Get list of variables in the variable group by Variable Group Id. You can find Variable Group Id in the URL of the Variable Group detail page.

$variablesJson = az pipelines variable-group variable list `
    --group-id <YOUR_VARIABLE_GROUP_ID> `
    --org https://dev.azure.com/<YOUR_ORGANIZATION>/ `
    --project <YOUR_PROJECT>

If we output $variablesJson, it would be something like following.

{
  "SomeOptions__Key1": {
    "isSecret"null,
    "value""<Value1>"
  },
  "SomeOptions__Key2": {
    "isSecret"null,
    "value""<Value2>"
  }
}

Convert the $variablesJson to app settings format that Azure App Service expects.

$variablesAppSettings = $variablesJson `
    | ConvertFrom-Json `
    | ForEach-Object { $_.PSObject.Properties } `
    | ForEach-Object ` {
        $key = $_.Name
        $value = $_.Value.value
        @{ 
            name = $key;
            slotSetting = $false;
            value = $value 
        }
}

Save the app settings to a temporary file.

ConvertTo-Json $variablesAppSettings | Out-File "$(System.DefaultWorkingDirectory)\appsettings-updated.json"

appsettings-updated.json would look like below.

[
  {
    "name""SomeOptions__Key1",
    "value""<Value1>",
    "slotSetting"false
  },
  {
    "name""SomeOptions__Key2",
    "value""<Value2>",
    "slotSetting"false
  }
]

Now finally update the app settings in the web app.

$resourceGroup = "<resourceGroup>"
$webAppName = "<webAppName>"

az webapp config appsettings set `
    --resource-group $resourceGroup `
    --name $webAppName `
    --settings "@$(System.DefaultWorkingDirectory)\appsettings-updated.json"

Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, February 10, 2025

Visual Studio 2022: HTTP Files and Request Variables

It's a very common scenario that we want to call an endpoint and use the result in subsequent requests. In this post, let's see how we can make use of Request Variables in .http files in Visual Studio 2022 to achieve that. Request Variable is a special kind of a variable (see my previous post for use of variables Visual Studio 2022: HTTP Files and Variables).

So let's start. The first step is to give a request a name.
@HostAddress = http://localhost:5200

### CREATE
# @name createEmployee

POST {{HostAddress}}/employees
Content-Type: application/json
{
   "firstName": "John",
   "lastName": "Doe"
}
Here, the comment: # @name createEmployee which is located just before the request specifies the name of the request. You can use following syntax if you prefer, which is also valid.
// @name createEmployee
Now we can reference this particular named request (createEmployee) using following syntax.
{{<requestName>.(response|request).(body|headers).(*|JSONPath|XPath|<headerName>)}}
For example, say the above endpoint is returning a JSON response, something like the following.
POST: Employee
Now I can use the returned id to Get the employee by Id.
### GET by Id
GET {{HostAddress}}/employees/{{createEmployee.response.body.$.id}}
Accept: application/json
GET: Employee
I can even use the request variable in the request body. Say I want to update the existing employee with a different lastName.
### UPDATE
PUT {{HostAddress}}/employees/{{createEmployee.response.body.$.id}}
Content-Type: application/json
{
    "id": "{{createEmployee.response.body.$.id}}",
    "firstName": "{{createEmployee.response.body.$.firstName}}",
    "lastName": "Bloggs"
}
PUT: Employee

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, February 5, 2025

Azure DevOps Classic Release Pipelines: Using Task Group Parameters to Control Task Execution

In this post let's see how we can use Parameters in a Task Group to control Task execution in Azure DevOps Classic Release Pipelines.

Let's say we have a Task Group that accepts following parameter.
Task Group Parameter
Now based on the value (true/false) passed in for this parameter, say I want to skip a particular Task. For that, we can use tasks Control Options -> Run this task -> Custom conditions.
Control Options -> Run this task -> Custom conditions
First step is initializing a release level variable with the value of the parameter. Note: I couldn't figure out how to access parameters directly in the condition, hence using a variable. If you find out a way, please do leave a comment. 

We can add in a PoweShell Task and do follows to initialize a variable.
Write-Host "##vso[task.setvariable variable=varIsSkipTask]$(IsSkipTask)"
Set Variable
And now, we can use the variable in custom condition as follows.
and(succeeded(), ne(variables['varIsSkipTask'], 'true'))
Control Options -> Run this task -> Custom conditions: Skip Task
And that's it.

Now when I run a release with IsSkipTask = true ,
IsSkipTask = true
Task is Skipped.
Task is skipped
Else,
Task is not skipped
Task is not getting skipped.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, January 24, 2025

.NET: How to Configure JsonSerializerOptions Correctly in a Builder ServiceCollection

Recently saw an issue in one of our applications that Configures JsonSerializerOptions as part of the builders ServiceCollection (FunctionsApplicationBuilderWebApplicationBuilder etc). 

To give a minimal example, say we want to configure JsonSerializerOptions.WriteIndented to true (in configuration it's false by default).
FunctionsApplicationBuilder builder = FunctionsApplication.CreateBuilder(args);

builder.Services.Configure<JsonSerializerOptions>(options =>
{
    options = new JsonSerializerOptions
    {
        WriteIndented = true,
    };
});
Above seems correct and could be easily unnoticeable in reviewing a PR.

But above is actually incorrect. If you resolve the JsonSerializerOptions and inspect the value of WriteIndented, it would still be false.
JsonSerializerOptions configuredJsonSerializerOptions = builder.Services.BuildServiceProvider().GetService<IOptions<JsonSerializerOptions>>().Value;

// This will be false.
bool isWriteIndented = configuredJsonSerializerOptions.WriteIndented;
The correct way is,
builder.Services.Configure<JsonSerializerOptions>(options =>
{
    options.WriteIndented = true;
});
That is rather than initializing a new JsonSerializerOptions with required configurations, update the existing JsonSerializerOptions that was supplied in the delegate.

Now JsonSerializerOptions will have the correct value.
JsonSerializerOptions configuredJsonSerializerOptions = builder.Services.BuildServiceProvider().GetService<IOptions<JsonSerializerOptions>>().Value;

// This will be true.
bool isWriteIndented = configuredJsonSerializerOptions.WriteIndented;
This is a simple mistake that could potentially cause a lot of issues, so hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, January 20, 2025

EF Core 9.0: Reading EntityConfigurations from Classes with Non-Public Constructors

In this post let's have a look at another feature in EF Core 9.0.

Consider the following code example.
public record Employee
{
    public int Id { getset}

    public string Name { getset}

    private class EmployeeConfiguration : IEntityTypeConfiguration<Employee>
    {
        private EmployeeConfiguration() { }

        public void Configure(EntityTypeBuilder<Employee> builder)
        {
            builder.Property(e => e.Name)
                .IsRequired()
                .HasMaxLength(50);
        }
    }
}
Prior to EF Core 9.0, if you try to ApplyConfigurationsFromAssembly, above EmployeeConfiguration will not be discovered. Because the entity configurations had to be types with a public, parameterless constructor.

But with EF Core 9.0, above EmployeeConfiguration will be discovered and we don't need to have a have a public type anymore.

While maintaining Entity Configuration along with Entity seem polluting the domain, I can certainly see an advantage. We now no longer have to look at a different file to see a particular entities configurations, everything is in one place. 

At least now we don't have a code constraint, the developer can decide which approach (public type vs private type) they are going to take.

Happy Coding.

Regards,
Jaliya

Thursday, January 16, 2025

Serialization in Azure.AI.DocumentIntelligence Version 1.0.0

Azure Document Intelligence client library for .NET, Azure.AI.DocumentIntelligence 1.0.0 was released few weeks ago with some significant refactoring to existing beta packages, and it has some breaking changes. 

One of the most important ones is Serialization in different options. 

Consider the following for an example.

using Azure.AI.DocumentIntelligence;
using System.Text.Json;

JsonSerializerOptions jsonSerializerOptions = new()
{
    WriteIndented = true
};

Dictionary<stringClassifierDocumentTypeDetails> documentTypes = new()
{
    { "Document1"new(new BlobFileListContentSource(new Uri("https://www.some-uri.com")"fileList1")) },
    { "Document2"new(new BlobFileListContentSource(new Uri("https://www.some-uri.com")"fileList2")) }
};

BuildClassifierOptions buildClassifierOptions = new("someClassifierId"documentTypes);

string json = JsonSerializer.Serialize(buildClassifierOptionsjsonSerializerOptions);
Console.WriteLine(json);
//{
//  "ClassifierId": "someClassifierId",
//  "Description": null,
//  "BaseClassifierId": null,
//  "DocumentTypes": {
//        "Document1": {
//            "BlobSource": null,
//      "BlobFileListSource": {
//                "ContainerUri": "https://www.some-uri.com",
//        "FileList": "fileList1"
//      },
//      "SourceKind": null
//        },
//    "Document2": {
//            "BlobSource": null,
//      "BlobFileListSource": {
//                "ContainerUri": "https://www.some-uri.com",
//        "FileList": "fileList2"
//      },
//      "SourceKind": null
//    }
//    },
//  "AllowOverwrite": null
//}

buildClassifierOptions = JsonSerializer.Deserialize<BuildClassifierOptions>(jsonjsonSerializerOptions);

The above will error out on Deserialize; with an error like "Unhandled exception. System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported".

Same error will happen with different types of Options.

In order for Serialization to work, we need to add JsonModelConverter to JsonSerializerOptions, a custom JsonConverter that is already available in System.ClientModel.

using System.ClientModel.Primitives;
using System.Text.Json;

JsonSerializerOptions jsonSerializerOptions = new()
{
    WriteIndented = true,
    Converters = { new JsonModelConverter() } // Converter to handle IJsonModel<T> types
};

More read:
   System.ClientModel-based ModelReaderWriter samples

Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, January 13, 2025

EF Core 9.0: Breaking Change in Migration Idempotent Scripts

In this post, let's have a look at a breaking change in EF Core 9.0 related to migration idempotent scripts.

Consider the following scenario.

Say we have a simple model like this.
public record Employee
{
    public int Id { getset}
    public string Name { getset}
}
And I have added a migration and have it applied in the database.

Now let's say I want update the model adding a new property, and need to update the value of existing rows by running a Custom SQL. Usually what we would do is after adding a migration, add code to execute the custom SQL, something like follows:
// Updated schema
public record Employee
{
    public int Id { getset}

    public string Name { getset}

    // New property
    public string Department { getset}
}
Modify migration to execute SQL statements to update existing rows: 
public partial class Secondary : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name"Department",
            table"Employees",
            type"nvarchar(max)",
            nullablefalse,
            defaultValue"");

        // Update existing records
        migrationBuilder.Sql("UPDATE Employees SET Department = 'IT'");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name"Department",
            table"Employees");
    }
}
With EF Core 9.0, if you create a idempotent script and execute it in SSMS (or use invoke-sqlcmd) , this is going to throw an error "Invalid column name 'Department'".
Invalid column name 'X'
The script is going to look like below:
--EF Core 9.0

IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
    CREATE TABLE [__EFMigrationsHistory] (
        [MigrationId] nvarchar(150) NOT NULL,
        [ProductVersion] nvarchar(32) NOT NULL,
        CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
    );
END;
GO

BEGIN TRANSACTION;
IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183107_Initial'
)
BEGIN
    CREATE TABLE [Employees] (
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NOT NULL,
        CONSTRAINT [PK_Employees] PRIMARY KEY ([Id])
    );
END;

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183107_Initial'
)
BEGIN
    INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
    VALUES (N'20250107183107_Initial', N'9.0.0');
END;

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    ALTER TABLE [Employees] ADD [Department] nvarchar(max) NOT NULL DEFAULT N'';
END;

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    UPDATE Employees SET Department = 'IT'
END;

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
    VALUES (N'20250107183141_Secondary', N'9.0.0');
END;

COMMIT;
GO
If we compare this to behavior of EF Core 8.x, the script EF Core 8.x generate will look like below:
--EF Core 8.x

IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
    CREATE TABLE [__EFMigrationsHistory] (
        [MigrationId] nvarchar(150) NOT NULL,
        [ProductVersion] nvarchar(32) NOT NULL,
        CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
    );
END;
GO

BEGIN TRANSACTION;
GO

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183107_Initial'
)
BEGIN
    CREATE TABLE [Employees] (
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NOT NULL,
        CONSTRAINT [PK_Employees] PRIMARY KEY ([Id])
    );
END;
GO

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183107_Initial'
)
BEGIN
    INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
    VALUES (N'20250107183107_Initial', N'8.0.11');
END;
GO

COMMIT;
GO

BEGIN TRANSACTION;
GO

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    ALTER TABLE [Employees] ADD [Department] nvarchar(max) NOT NULL DEFAULT N'';
END;
GO

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    UPDATE Employees SET Department = 'IT'
END;
GO

IF NOT EXISTS (
    SELECT * FROM [__EFMigrationsHistory]
    WHERE [MigrationId] = N'20250107183141_Secondary'
)
BEGIN
    INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
    VALUES (N'20250107183141_Secondary', N'8.0.11');
END;
GO

COMMIT;
GO
You can see in the script generated by EF Core 9.0,  the GO statements after Control Statements (BEGIN...END) aren't no longer there and that is by design.

But then, because of that we are getting the compile error.

The work around is, in the migration, use EXEC as follows:
// Update existing records
migrationBuilder.Sql("EXEC('UPDATE Employees SET Department = ''IT''')");
Hope this helps.

Happy Coding.

Regards,
Jaliya

Sunday, December 29, 2024

EF Core 9.0: Introducing EF.Parameter(T)

In this post, let's have a look EF.Parameter<T>(T) method that was introduced with EF 9.0.

Consider the following simple LINQ query.
async Task<List<Employee>> GetEmployees(int employeeId)
{
    return await context.Employees
        .Where(e => e.Id == employeeId && e.IsActive == true)
        .ToListAsync();
}
This would generate a SQL query as follows.
--Executed DbCommand (24ms) [Parameters=[@__employeeId_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [e].[Id], [e].[IsActive]
FROM [Employees] AS [e]
WHERE [e].[Id] = @__employeeId_0 AND [e].[IsActive] = CAST(AS bit)
Here you can see the employeeId is parameterized, and IsActive is a constant. When a constant is being used database engine can cache the query resulting a more efficient query.

However if for some reason you want to parameterize the value, you can use EF.Parameter<T>(T).
async Task<List<Employee>> GetEmployees(int employeeId)
{
    return await context.Employees
        .Where(e => e.Id == employeeId && e.IsActive == EF.Parameter(true))
        .ToListAsync();
}
This would generate a SQL query as follows.
--Executed DbCommand (26ms) [Parameters=[@__employeeId_0='?' (DbType = Int32), @__p_1='?' (DbType = Boolean)], CommandType='Text', CommandTimeout='30']
SELECT [e].[Id], [e].[IsActive]
FROM [Employees] AS [e]
WHERE [e].[Id] = @__employeeId_0 AND [e].[IsActive] = @__p_1
While we are on this topic, EF Core 8.0.2 introduced EF.Constant<T>(T) method which forces EF to use a constant even if a parameter would be used by default.
async Task<List<Employee>> GetEmployees(int employeeId)
{
    return await context.Employees
        .Where(e => e.Id == EF.Constant(employeeId) && e.IsActive == EF.Parameter(true))
        .ToListAsync();
}
And this would generate a SQL query as follows.
--Executed DbCommand (18ms) [Parameters=[@__p_1='?' (DbType = Boolean)], CommandType='Text', CommandTimeout='30']
SELECT [e].[Id], [e].[IsActive]
FROM [Employees] AS [e]
WHERE [e].[Id] = 10 AND [e].[IsActive] = @__p_1
Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, December 12, 2024

ASP.NET Core 9.0: Microsoft.AspNetCore.OpenApi and Swagger UI

With the release of .NET 9.0, you might have noticed for ASP.NET Core Web Application templates in Visual Studio, we no longer have Swagger UI configured.

It basically looks like below.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

...

By default route to the OpenAPI specification will be https://<applicationUrl>/openapi/v1.json.

So far ASP.NET Core team hasn't announced (or I have not seen) their own UI for viewing OpenAPI specification. 

We can still use Swagger UI to view the OpenAPI specification.

First we need to install the package: Swashbuckle.AspNetCore.SwaggerUI.

Then we can do either one of this.

  • Update the Open API document route to the route Swagger UI expects
// Option 1: Update the Open API document route to the route Swagger UI expects
app.MapOpenApi("swagger/v1/swagger.json");
app.UseSwaggerUI();
Update the Open API document route to the route Swagger UI expects
  • Update Swagger UI to use the Open API document route
// Option 2: Update Swagger UI to use the Open API document route
app.MapOpenApi();
app.UseSwaggerUI(x =>
{
    x.SwaggerEndpoint("/openapi/v1.json""My API");
});
Update Swagger UI to use the Open API document route

Hope this helps.

Happy Coding.

Regards,
Jaliya