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