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

Monday, December 9, 2024

Use of PipelinePolicies in Azure.AI.OpenAI.AzureOpenAIClient

In this post let's see how we can make use of PipelinePolicies in AzureOpenAIClient. Even though we specifically talk about AzureOpenAIClient in this post, the concept will be the same among many Clients in Azure SDK.

When a client in Azure SDK sends a request to Azure Service/API, the request travels through a pipeline which consists of a set of policies that get to modify the request before it's being sent and observe the response after it's received.

In order to add a Custom policy, we need to implement the abstract class PipelinePolicy.

public class CustomLoggingPolicy : PipelinePolicy
{
    // For synchronous processing // Not implementing as most of the time we are calling async methods in the client
    public override void Process(PipelineMessage messageIReadOnlyList<PipelinePolicy> pipelineint currentIndex)
    {
        ProcessNext(messagepipelinecurrentIndex);
    }

    // For asynchronous processing
    public override async ValueTask ProcessAsync(PipelineMessage messageIReadOnlyList<PipelinePolicy> pipelineint currentIndex)
    {
        Console.WriteLine($"Sending Request to {message.Request.Method}{message.Request.Uri}");

        await ProcessNextAsync(messagepipelinecurrentIndex);

        if (message.Response?.IsError == true)
        {
            Console.WriteLine($"Request to {message.Request.Method}{message.Request.Uri} failed.");
            Console.WriteLine(new
            {
                message.Response.ReasonPhrase,
                message.Response.Content,
            });
        }
        else
        {
            Console.WriteLine($"Request to {message.Request.Method}{message.Request.Uri} completed.");
        }
    }
}

Here I have added CustomLoggingPolicy for demo purposes. A key thing to remember is, we need to call the  ProcessNext/ProcessNextAsync method to pass the control to the next PipelinePolicy.

We can now make use of the CustomLoggingPolicy as follows.

string deploymentName = "gpt-4o";

AzureOpenAIClientOptions azureOpenAIClientOptions = new();
azureOpenAIClientOptions.AddPolicy(new CustomLoggingPolicy()PipelinePosition.BeforeTransport);

AzureOpenAIClient azureOpenAiClient =
    new(new Uri("<openai-endpoint>")new DefaultAzureCredential()azureOpenAIClientOptions);

ChatClient chatClient = azureOpenAiClient.GetChatClient(deploymentName);

List<ChatMessage> chatMessages =
[
    // messages
];

ChatCompletionOptions chatCompletionOptions = new()
{
    // options
};

ClientResult<ChatCompletion> clientResult
   = await chatClient.CompleteChatAsync(chatMessageschatCompletionOptions);
CustomLoggingPolicy
Say you want to add a Custom Retry policy, then you can implement a class that inherits from ClientRetryPolicy and override ShouldRetryAsync method.

public class CustomClientRetryPolicy : ClientRetryPolicy
{
    protected override async ValueTask<bool> ShouldRetryAsync(PipelineMessage messageExceptionexception)
    {
        if (message.Response.Status == (int)HttpStatusCode.RequestEntityTooLarge)
        {
            return false;
        }

        return await base.ShouldRetryAsync(messageexception);
    }
}
Usage:
AzureOpenAIClientOptions azureOpenAIClientOptions = new();
...
azureOpenAIClientOptions.RetryPolicy = new CustomClientRetryPolicy();

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, December 4, 2024

EF Core 9.0: UseSeeding and UseAsyncSeeding

EF Core 9.0 (EF 9) introduced new methods UseSeeding and UseAsyncSeeding to seed the initial data.

Let's have a look at an example.

Consider the following MyDbContext.

public record Blog
{
    public int Id { getset}

    public string Name { getset}
}

public class MyDbContext : DbContext
{
    public DbSet<Blog> Blogs { getset}

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>(x =>
        {
            x.HasData
            (
               new Blog
               {
                   Id = 1,
                   Name = "Some Blog"
               }
            );
        });
    }
}

Here in the above example, some data is seeded through Model-managed data which is handly when we want to include seed data in migrations as well and it was available for as far as I can remember.

Now let's see where UseSeeding and UseAsyncSeeding come in.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer("<ConnectionString>")
        .UseSeeding((context_) =>
        {
            Blogblog = context.Set<Blog>().SingleOrDefault(b => b.Name == "Some Other Blog");
            if (blog is null)
            {
                context.Set<Blog>().Add(new Blog { Name = "Some Other Blog" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context_cancellationToken) =>
        {
            Blogblog = await context.Set<Blog>().SingleOrDefaultAsync(b => b.Name == "Some Other Blog"cancellationToken);
            if (blog is null)
            {
                context.Set<Blog>().Add(new Blog { Name = "Some Other Blog" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });
}

As you can see, the new seeding methods are available as part of configuring DbContextOptions. Unlike Model-managed data, you can seed data for multiple DbSets all from a single place. These methods will get called as part of EnsureCreated operation, Migrate and dotnet ef database update command, even if there are no model changes and no migrations were applied.

Now let's see this in action.

using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

foreach (Blog blog in context.Blogs)
{
    Console.WriteLine(blog.Name);
}

// Output:
//Some Blog
//Some Other Blog

And a note from the official documentation:

UseSeeding is called from the EnsureCreated method, and UseAsyncSeeding is called from the EnsureCreatedAsync method. When using this feature, it is recommended to implement both UseSeeding and UseAsyncSeeding methods using similar logic, even if the code using EF is asynchronous. EF Core tooling currently relies on the synchronous version of the method and will not seed the database correctly if the UseSeeding method is not implemented.

More read:
   EF Core: 
Data Seeding

Happy Coding.

Regards,
Jaliya

Friday, November 29, 2024

Blazor Web App: Adding Custom Claims to Current User

I recently had a requirement where I want to add Custom Claims to the current user in a Blazor Web Application. I used IClaimsTransformation, but it doesn't work with Blazor. Instead, the correct option for Blazor is to implement a custom AuthenticationStateProvider.

public class ApplicationAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    private readonly IUserService _userService;

    public ApplicationAuthenticationStateProvider(AuthenticationStateProvider AuthenticationStateProvider, IUserService userService)
    {
        _authenticationStateProvider = AuthenticationStateProvider;
        _userService = userService;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        AuthenticationState authenticationState =  await _authenticationStateProvider.GetAuthenticationStateAsync();
        if (authenticationState.User.Identity?.IsAuthenticated != true)
        {
            return authenticationState;
        }

        // Check if user already has the UserId claim
        if (authenticationState.User.HasClaim(x => x.Type == ApplicationClaimTypes.UserId))
        {
            return authenticationState;
        }

        // TODO: Get the user from the database using current claims and IUserService
        string applicationUserId = "someUserId";

        ClaimsIdentity claimsIdentity = new ClaimsIdentity();
        claimsIdentity.AddClaim(new Claim(ApplicationClaimTypes.UserId, applicationUserId));

        authenticationState.User.AddIdentity(claimsIdentity);

        return authenticationState;
    }
}

Register ApplicationAuthenticationStateProvider,

builder.Services.AddScoped<ApplicationAuthenticationStateProvider>();

Use,

// This is some service where you want to use authenticationState
public class BlazorIdentityService : IIdentityService
{
    private readonly ApplicationAuthenticationStateProvider _applicationAuthenticationStateProvider;

    public BlazorIdentityService(ApplicationAuthenticationStateProvider applicationAuthenticationStateProvider)
    {
        _applicationAuthenticationStateProvider = applicationAuthenticationStateProvider;
    }

    public async Task<string> GetCurrentUserId()
    {
        AuthenticationState authenticationState =  await _applicationAuthenticationStateProvider?.GetAuthenticationStateAsync();

        return authenticationState.User?.Claims?.SingleOrDefault(x => x.Type == ApplicationClaimTypes.UserId)?.Value;
    }
}

Hope this helps.

Happy Coding.

Regards,
Jaliya