.NET: Address Azure Blob storage review feedback

Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5
This commit is contained in:
Roger Barreto
2026-08-20 11:24:42 +01:00
parent 7c65e77fd0
commit b29237b2fa
8 changed files with 76 additions and 26 deletions
+1
View File
@@ -19,6 +19,7 @@
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AzureStorage\\Microsoft.Agents.AI.Hosting.AzureStorage.csproj",
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
"src\\Microsoft.Agents.AI.LocalCodeAct\\Microsoft.Agents.AI.LocalCodeAct.csproj",
@@ -158,7 +158,7 @@ public sealed class AzureBlobAgentSessionStore : AgentSessionStore
try
{
await initializationTask.WaitAsync(cancellationToken).ConfigureAwait(false);
await WaitWithCancellationAsync(initializationTask, cancellationToken).ConfigureAwait(false);
}
catch when (initializationTask.IsFaulted || initializationTask.IsCanceled)
{
@@ -187,8 +187,47 @@ public sealed class AzureBlobAgentSessionStore : AgentSessionStore
: $"{this._blobNamePrefix}/{baseName}";
}
private static async Task WaitWithCancellationAsync(Task task, CancellationToken cancellationToken)
{
if (task.IsCompleted || !cancellationToken.CanBeCanceled)
{
await task.ConfigureAwait(false);
return;
}
var cancellationTaskSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled()))
{
Task completedTask = await Task.WhenAny(task, cancellationTaskSource.Task).ConfigureAwait(false);
await completedTask.ConfigureAwait(false);
}
}
private static string ComputeKey(string value)
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)));
{
byte[] input = Encoding.UTF8.GetBytes(value);
#if NET8_0_OR_GREATER
return Convert.ToHexString(SHA256.HashData(input));
#else
using SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(input);
char[] result = new char[hash.Length * 2];
for (int index = 0; index < hash.Length; index++)
{
byte valueByte = hash[index];
result[index * 2] = ToHexChar(valueByte >> 4);
result[(index * 2) + 1] = ToHexChar(valueByte & 0x0F);
}
return new string(result);
#endif
}
#if !NET8_0_OR_GREATER
private static char ToHexChar(int value)
=> (char)(value < 10 ? '0' + value : 'A' + value - 10);
#endif
private static string? NormalizePrefix(string? prefix)
{
@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<InjectSharedThrow>true</InjectSharedThrow>
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
#if NET8_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -8,13 +9,15 @@ using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Agents.AI.Hosting.AzureStorage.Tests;
using Shared.IntegrationTests;
#endif
namespace Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests;
public sealed class AzureBlobAgentSessionStoreIntegrationTests
{
#if NET8_0_OR_GREATER
[Fact(Skip = "Requires a provisioned Azure Storage account and data-plane permissions in CI.")]
public async Task HostedAgentThroughFakeKestrel_PersistsSessionInLiveBlobStorageAsync()
public async Task HostedAgentThroughTestServer_PersistsSessionInLiveBlobStorageAsync()
{
// Arrange
string? endpoint = Environment.GetEnvironmentVariable("AZURE_STORAGE_BLOB_ENDPOINT");
@@ -29,9 +32,9 @@ public sealed class AzureBlobAgentSessionStoreIntegrationTests
try
{
// Act
await using FakeKestrelAgentHost host =
await FakeKestrelAgentHost.StartAsync(containerClient);
FakeKestrelAgentHost.FakeKestrelRunResult result = await host.RunTwoTurnsAsync();
await using FakeTestAgentHost host =
await FakeTestAgentHost.StartAsync(containerClient);
FakeTestAgentHost.FakeTestAgentRunResult result = await host.RunTwoTurnsAsync();
List<BlobItem> blobs = [];
await foreach (BlobItem blob in containerClient.GetBlobsAsync())
{
@@ -57,4 +60,5 @@ public sealed class AzureBlobAgentSessionStoreIntegrationTests
await containerClient.DeleteIfExistsAsync();
}
}
#endif
}
@@ -1,24 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworksCore);net472</TargetFrameworks>
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<PackageReference Include="AGUI.Abstractions" />
<PackageReference Include="AGUI.Client" />
<PackageReference Include="AGUI.Server" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AzureStorage\Microsoft.Agents.AI.Hosting.AzureStorage.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
</ItemGroup>
<ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Compile Include="..\Microsoft.Agents.AI.Hosting.AzureStorage.TestUtilities\*.cs" LinkBase="TestUtilities" />
</ItemGroup>
@@ -21,20 +21,20 @@ using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.AzureStorage.Tests;
internal sealed class FakeKestrelAgentHost : IAsyncDisposable
internal sealed class FakeTestAgentHost : IAsyncDisposable
{
private const string AgentName = "azure-blob-session-agent";
private readonly WebApplication _app;
private readonly HttpClient _client;
private FakeKestrelAgentHost(WebApplication app, HttpClient client)
private FakeTestAgentHost(WebApplication app, HttpClient client)
{
this._app = app;
this._client = client;
}
public static async Task<FakeKestrelAgentHost> StartAsync(BlobContainerClient containerClient)
public static async Task<FakeTestAgentHost> StartAsync(BlobContainerClient containerClient)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
@@ -52,16 +52,16 @@ internal sealed class FakeKestrelAgentHost : IAsyncDisposable
HttpClient client = server.CreateClient();
client.BaseAddress = new Uri("http://localhost/agent");
return new FakeKestrelAgentHost(app, client);
return new FakeTestAgentHost(app, client);
}
public async Task<FakeKestrelRunResult> RunTwoTurnsAsync()
public async Task<FakeTestAgentRunResult> RunTwoTurnsAsync()
{
var chatClient = new AGUIChatClient(new(this._client, ""));
AIAgent clientAgent = chatClient.AsAIAgent(
instructions: null,
name: "client-agent",
description: "Client for the fake Kestrel agent host.",
description: "Client for the in-memory test agent host.",
tools: []);
AgentSession clientSession = await clientAgent.CreateSessionAsync();
@@ -115,7 +115,7 @@ internal sealed class FakeKestrelAgentHost : IAsyncDisposable
await this._app.DisposeAsync();
}
internal sealed record FakeKestrelRunResult(
internal sealed record FakeTestAgentRunResult(
string FirstResponse,
string SecondResponse);
@@ -7,7 +7,9 @@ using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
#if NET8_0_OR_GREATER
using Microsoft.Agents.AI.Hosting.AzureStorage.Tests;
#endif
namespace Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests;
@@ -230,15 +232,16 @@ public sealed class AzureBlobAgentSessionStoreTests : IAsyncLifetime
Assert.Equal(16, blobs.Count);
}
#if NET8_0_OR_GREATER
[Fact]
public async Task HostedAgentThroughFakeKestrel_PersistsSessionInAzuriteAsync()
public async Task HostedAgentThroughTestServer_PersistsSessionInAzuriteAsync()
{
// Arrange
await using FakeKestrelAgentHost host =
await FakeKestrelAgentHost.StartAsync(this._containerClient);
await using FakeTestAgentHost host =
await FakeTestAgentHost.StartAsync(this._containerClient);
// Act
FakeKestrelAgentHost.FakeKestrelRunResult result = await host.RunTwoTurnsAsync();
FakeTestAgentHost.FakeTestAgentRunResult result = await host.RunTwoTurnsAsync();
List<BlobItem> blobs = [];
await foreach (BlobItem blob in this._containerClient.GetBlobsAsync())
{
@@ -258,6 +261,7 @@ public sealed class AzureBlobAgentSessionStoreTests : IAsyncLifetime
Assert.Contains("turnCounter", persistedSession, StringComparison.Ordinal);
Assert.Contains("\"count\":2", persistedSession, StringComparison.Ordinal);
}
#endif
[Fact]
public void Constructor_BlobNamePrefixExceedsAzureLimit_Throws()
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworksCore);net472</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<PackageReference Include="AGUI.Abstractions" />
<PackageReference Include="AGUI.Client" />
<PackageReference Include="AGUI.Server" />
@@ -13,10 +13,10 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AzureStorage\Microsoft.Agents.AI.Hosting.AzureStorage.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
</ItemGroup>
<ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Compile Include="..\Microsoft.Agents.AI.Hosting.AzureStorage.TestUtilities\*.cs" LinkBase="TestUtilities" />
</ItemGroup>