Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c3f6f5c
support retries
DeagleGross Aug 12, 2025
a4922fb
Merge branch 'main' into dmkorolev/cosmos-retries
DeagleGross Aug 15, 2025
c4b247f
tests + registration options
DeagleGross Aug 15, 2025
56e469c
Merge branch 'main' into dmkorolev/cosmos-retries
DeagleGross Aug 15, 2025
c9de82c
fix ordering ..
DeagleGross Aug 15, 2025
200b699
Merge branch 'dmkorolev/cosmos-retries' of https://github.com/microso…
DeagleGross Aug 15, 2025
68a69e9
HK + update packages
DeagleGross Aug 15, 2025
9bdb789
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 15, 2025
362aa5d
fix paths
DeagleGross Aug 15, 2025
65407d1
Update dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI…
DeagleGross Aug 15, 2025
c602bec
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 15, 2025
30b254f
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 15, 2025
fb02856
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 18, 2025
ba92aa4
re create project and fix some pk usage
DeagleGross Aug 18, 2025
c55344b
fix all tests
DeagleGross Aug 18, 2025
fefabd4
try workflow?
DeagleGross Aug 19, 2025
d5ece1d
wip 1
DeagleGross Aug 19, 2025
dbe5230
fix definition
DeagleGross Aug 19, 2025
7166b30
try with cosmos_use_emulator env?
DeagleGross Aug 19, 2025
546181b
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 19, 2025
04e5eaf
try ignore SSL errors?
DeagleGross Aug 19, 2025
f9a1ec3
other cert verifications
DeagleGross Aug 19, 2025
e51342b
hardcode to 8081?
DeagleGross Aug 19, 2025
edd8013
proper valuation of ENV
DeagleGross Aug 19, 2025
6823c96
logging
DeagleGross Aug 19, 2025
7a8338a
ensure db exsists for CI
DeagleGross Aug 19, 2025
4f82ae1
bump
DeagleGross Aug 19, 2025
9740f55
cleanup
DeagleGross Aug 19, 2025
5ca3217
fix usage
DeagleGross Aug 19, 2025
b8d03ea
nit comment
DeagleGross Aug 19, 2025
f7fff63
try only release for stability?
DeagleGross Aug 20, 2025
5e8eadc
Merge branch 'main' into dmkorolev/partition-key
DeagleGross Aug 20, 2025
df9a09e
try skip some flaky tests
DeagleGross Aug 20, 2025
62cffe8
merge main
DeagleGross Aug 20, 2025
a97693b
merge fixes + rollback container
DeagleGross Aug 20, 2025
4d91baf
reimplement with iasyncdisposable pattern
DeagleGross Aug 20, 2025
682decc
Merge branch 'main' into dmkorolev/cosmos-retries
DeagleGross Aug 20, 2025
a5489d9
Merge branch 'main' into dmkorolev/cosmos-retries
DeagleGross Aug 21, 2025
b693e26
remove example doc struct
DeagleGross Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions .github/workflows/dotnet-cosmosdb-integration-tests.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
#
# This workflow runs Cosmos DB integration tests using the Cosmos DB emulator.
#

name: dotnet-cosmosdb-integration-tests

on:
workflow_dispatch:
pull_request:
branches: ["main", "feature*"]
paths:
- dotnet/tests/CosmosDB.IntegrationTests/**
- dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/**
- '.github/workflows/dotnet-cosmosdb-integration-tests.yml'
merge_group:
branches: ["main"]
push:
branches: ["main", "feature*"]
paths:
- dotnet/tests/CosmosDB.IntegrationTests/**
- dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/**
- '.github/workflows/dotnet-cosmosdb-integration-tests.yml'
schedule:
- cron: "0 2 * * *" # Run at 2 AM UTC daily

env:
COSMOSDB_TESTS_USE_EMULATOR_CICD: "true"

jobs:
build-and-test:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Release }
# - { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Debug }

services:
cosmosdb:
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest
ports:
- 8081:8081
env:
AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "false"
AZURE_COSMOS_EMULATOR_PARTITION_COUNT: "20" # the more the better for stable tests

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
- name: Setup dotnet
uses: actions/setup-dotnet@v4.3.1
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build dotnet solutions
shell: bash
run: |
export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
for solution in $SOLUTIONS; do
dotnet build $solution -c ${{ matrix.configuration }} --warnaserror
done
- name: Package install check
shell: bash
# All frameworks are only built for the release configuration, so we only run this step for the release configuration
# and dotnet new doesn't support net472
if: matrix.configuration == 'Release' && matrix.targetFramework != 'net472'
run: |
TEMP_DIR=$(mktemp -d)

export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
for solution in $SOLUTIONS; do
dotnet pack $solution /property:TargetFrameworks=${{ matrix.targetFramework }} -c ${{ matrix.configuration }} --no-build --no-restore --output "$TEMP_DIR/artifacts"
done

pushd "$TEMP_DIR"

# Create a new console app to test the package installation
dotnet new console -f ${{ matrix.targetFramework }} --name packcheck --output consoleapp

# Create minimal nuget.config and use only dotnet nuget commands
echo '<?xml version="1.0" encoding="utf-8"?><configuration><packageSources><clear /></packageSources></configuration>' > consoleapp/nuget.config

# Add sources with local first using dotnet nuget commands
dotnet nuget add source ../artifacts --name local --configfile consoleapp/nuget.config
dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile consoleapp/nuget.config

# Change to project directory to ensure local nuget.config is used
pushd consoleapp
dotnet add packcheck.csproj package Microsoft.Extensions.AI.Agents --prerelease
dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj

# Clean up
popd
popd
rm -rf "$TEMP_DIR"

- name: Wait for Cosmos DB Emulator to be ready
run: |
set -e
for i in $(seq 1 120); do
if curl -sk https://localhost:8081/_explorer/emulator.pem -o /dev/null; then
echo "Emulator is up."
break
fi
echo "Waiting for emulator... ($i/120)"
sleep 2
done

- name: Install emulator TLS certificate into system trust store
run: |
set -e
sudo apt-get update
sudo apt-get install -y ca-certificates curl openssl
# Fetch the PEM directly from the emulator's explorer endpoint
curl -sk https://localhost:8081/_explorer/emulator.pem -o cosmos-emulator.crt
# Install with the correct .crt extension so update-ca-certificates picks it up
sudo cp cosmos-emulator.crt /usr/local/share/ca-certificates/cosmos-emulator.crt
sudo update-ca-certificates

- name: Verify TLS now trusts the emulator
run: |
# Use -servername to avoid SNI warning and check verification
echo | openssl s_client -connect localhost:8081 -servername localhost 2>/dev/null | grep -E "Verify return code|subject=|issuer="
# Expect: "Verify return code: 0 (ok)"

- name: Run Cosmos DB Integration Tests
shell: bash
run: |
# Run the specific CosmosDB integration tests
dotnet test ./dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj \
-f ${{ matrix.targetFramework }} \
-c ${{ matrix.configuration }} \
--no-build \
-v Normal \
--logger trx \
--collect:"XPlat Code Coverage" \
--results-directory:"TestResults/Coverage/" \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute

# Generate test reports and check coverage
- name: Generate test reports
uses: danielpalme/ReportGenerator-GitHub-Action@5.4.11
with:
reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
targetdir: "./TestResults/Reports"
reporttypes: "HtmlInline;JsonSummary"

- name: Upload coverage report artifact
uses: actions/upload-artifact@v4
with:
name: CosmosDB-CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }}
path: ./TestResults/Reports
17 changes: 11 additions & 6 deletions dotnet/Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,14 +5,18 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>9.4.1</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Azure.* -->
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.3.1-preview.1.25305.6" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.0" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="9.4.0" />
<PackageVersion Include="Aspire.Hosting.Azure.CosmosDB" Version="9.4.0" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="9.3.1" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.3.1" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.4.1-preview.1.25408.4" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Azure.CosmosDB" Version="9.4.1" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.1" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.1" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.5" />
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
Expand DownExpand Up@@ -61,6 +65,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.8" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.61.0-preview" />
<!-- Agent SDKs -->
Expand Down
5 changes: 5 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
<Folder Name="/Solution Items/.github/workflows/">
<File Path="../.github/workflows/dotnet-build-and-test.yml" />
<File Path="../.github/workflows/dotnet-check-coverage.ps1" />
<File Path="../.github/workflows/dotnet-cosmosdb-integration-tests.yml" />
<File Path="../.github/workflows/dotnet-format.yml" />
</Folder>
<Folder Name="/Solution Items/docs/" />
Expand DownExpand Up@@ -121,6 +122,10 @@
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
</Folder>
<Folder Name="/Tests/" />
<Folder Name="/Tests/Cosmos/">
<Project Path="tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj" />
<Project Path="tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj" />
</Folder>
<Folder Name="/Tests/IntegrationTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.3.1" />
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireAppHostSdkVersion)" />

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,13 +11,6 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB;
/// the entire actor's state for optimistic concurrency control.
/// This document contains no actor state data. It only serves to track last modified
/// time and provide a single ETag for the actor's state.
///
/// Example structure:
/// {
/// "id": "rootdoc", // Root document ID (constant per actor partition)
/// "actorId": "actor-123", // Partition key (actor ID)
/// "lastModified": "2024-...", // Timestamp
/// }
/// </summary>
public sealed class ActorRootDocument
{
Expand All@@ -27,9 +20,13 @@ public sealed class ActorRootDocument
public string Id { get; set; } = default!;

/// <summary>
/// The actor ID.
/// The actor type.
Comment thread
DeagleGross marked this conversation as resolved.
/// </summary>
public string ActorId { get; set; } = default!;
public string ActorType { get; set; } = default!;
/// <summary>
/// The actor key.
/// </summary>
public string ActorKey { get; set; } = default!;

/// <summary>
/// The last modified timestamp.
Expand All@@ -55,9 +52,13 @@ public sealed class ActorStateDocument
public string Id { get; set; } = default!;

/// <summary>
/// The actor ID.
/// The actor type.
/// </summary>
public string ActorType { get; set; } = default!;
/// <summary>
/// The actor key.
/// </summary>
public string ActorId { get; set; } = default!;
public string ActorKey { get; set; } = default!;

/// <summary>
/// The logical key for the state entry.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB;
/// <summary>
/// Cosmos DB implementation of actor state storage.
/// </summary>
public class CosmosActorStateStorage : IActorStateStorage
public class CosmosActorStateStorage : IActorStateStorage, IAsyncDisposable
{
private readonly LazyCosmosContainer _lazyContainer;
private const string InitialEtag = "0"; // Initial ETag value when no state exists
Expand DownExpand Up@@ -47,8 +47,8 @@ public async ValueTask<WriteResponse> WriteStateAsync(
}

var container = await this._lazyContainer.GetContainerAsync().ConfigureAwait(false);
var batch = container.CreateTransactionalBatch(GetPartitionKey(actorId));
var actorIdStr = actorId.ToString();
var (partitionKey, actorType, actorKey) = BuildPartitionKey(actorId);
var batch = container.CreateTransactionalBatch(partitionKey);

// Add data operations to batch
foreach (var op in operations)
Expand All@@ -61,7 +61,8 @@ public async ValueTask<WriteResponse> WriteStateAsync(
var item = new ActorStateDocument
{
Id = docId,
ActorId = actorIdStr,
ActorType = actorType,
ActorKey = actorKey,
Key = set.Key,
Value = set.Value
};
Expand All@@ -83,7 +84,8 @@ public async ValueTask<WriteResponse> WriteStateAsync(
var newRoot = new ActorRootDocument
{
Id = RootDocumentId,
ActorId = actorId.ToString(),
ActorType = actorType,
ActorKey = actorKey,
LastModified = DateTimeOffset.UtcNow,
};

Expand All@@ -103,6 +105,7 @@ public async ValueTask<WriteResponse> WriteStateAsync(
var result = await batch.ExecuteAsync(cancellationToken).ConfigureAwait(false);
if (!result.IsSuccessStatusCode)
{
_ = result.ErrorMessage;
return new WriteResponse(eTag: string.Empty, success: false);
}

Expand DownExpand Up@@ -135,6 +138,8 @@ public async ValueTask<ReadResponse> ReadStateAsync(

// Read root document first to get actor-level ETag
string actorETag = await this.GetActorETagAsync(container, actorId, cancellationToken).ConfigureAwait(false);
var actorType = actorId.Type.ToString();
var actorKey = actorId.Key;

foreach (var op in operations)
{
Expand DownExpand Up@@ -162,14 +167,16 @@ public async ValueTask<ReadResponse> ReadStateAsync(
QueryDefinition query;
if (!string.IsNullOrEmpty(list.KeyPrefix))
{
query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorId = @actorId AND c.key != null AND STARTSWITH(c.key, @keyPrefix)")
.WithParameter("@actorId", actorId.ToString())
query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null AND STARTSWITH(c.key, @keyPrefix)")
.WithParameter("@actorType", actorType)
.WithParameter("@actorKey", actorKey)
.WithParameter("@keyPrefix", list.KeyPrefix);
}
else
{
query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorId = @actorId AND c.key != null")
.WithParameter("@actorId", actorId.ToString());
query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null")
.WithParameter("@actorType", actorType)
.WithParameter("@actorKey", actorKey);
}

var requestOptions = new QueryRequestOptions
Expand DownExpand Up@@ -212,7 +219,18 @@ public async ValueTask<ReadResponse> ReadStateAsync(
private const string RootDocumentId = "rootdoc";

private static PartitionKey GetPartitionKey(ActorId actorId)
=> new(actorId.ToString());
{
var (partitionKey, _, _) = BuildPartitionKey(actorId);
return partitionKey;
}

private static (PartitionKey partitionKey, string actorType, string actorKey) BuildPartitionKey(ActorId actorId)
{
var actorType = actorId.Type.ToString();
var actorKey = actorId.Key;
var partitionKey = new PartitionKeyBuilder().Add(actorType).Add(actorKey).Build();
return (partitionKey, actorType, actorKey);
}

/// <summary>
/// Gets the current ETag for the actor's root document.
Expand All@@ -234,4 +252,13 @@ private async ValueTask<string> GetActorETagAsync(Container container, ActorId a
return InitialEtag;
}
}

/// <summary>
/// Disposes the Cosmos DB container asynchronously.
/// </summary>
public async ValueTask DisposeAsync()
{
await this._lazyContainer.DisposeAsync().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
}
Loading
Loading