Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 30 additions & 3 deletions dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/TypeId.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;

Expand All @@ -11,12 +12,38 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
/// </summary>
public sealed class TypeId : IEquatable<TypeId>
{
private string? _assemblySimpleName;

/// <inheritdoc cref="System.Reflection.Assembly.FullName"/>
public string AssemblyName { get; }

/// <inheritdoc cref="Type.FullName"/>
public string TypeName { get; }

/// <summary>
/// Gets the simple (version-, culture-, and public-key-token-independent) name of the assembly,
/// derived from <see cref="AssemblyName"/>. This is used for identity comparisons and type
/// resolution so that checkpoints remain compatible across assembly version changes.
/// </summary>
internal string AssemblySimpleName => this._assemblySimpleName ??= NormalizeAssemblyName(this.AssemblyName);

/// <summary>
/// Extracts the simple assembly name from a (possibly fully-qualified) assembly name string,
/// discarding version, culture, and public key token. Falls back to the original value if it
/// cannot be parsed.
/// </summary>
private static string NormalizeAssemblyName(string assemblyName)
{
try
{
return new AssemblyName(assemblyName).Name ?? assemblyName;
}
catch (Exception)
{
return assemblyName;
}
}
Comment on lines +35 to +45

/// <summary>
/// Initializes a new instance of the <see cref="TypeId"/> class.
/// </summary>
Expand Down Expand Up @@ -58,11 +85,11 @@ public bool Equals(TypeId? other)
return true;
}

return this.AssemblyName == other.AssemblyName && this.TypeName == other.TypeName;
return this.AssemblySimpleName == other.AssemblySimpleName && this.TypeName == other.TypeName;
}

/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(this.AssemblyName, this.TypeName);
public override int GetHashCode() => HashCode.Combine(this.AssemblySimpleName, this.TypeName);

/// <inheritdoc />
public static bool operator ==(TypeId? left, TypeId? right) => left is null ? right is null : left.Equals(right);
Expand All @@ -78,7 +105,7 @@ public bool Equals(TypeId? other)
/// false.</returns>
public bool IsMatch(Type type)
{
return this.AssemblyName == type.Assembly.FullName
return this.AssemblySimpleName == type.Assembly.GetName().Name
&& this.TypeName == type.FullName;
Comment on lines +108 to 109
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ private static bool TryGetRequestEnvelope(ExternalRequest request, [NotNullWhen(
envelope = null;

TypeId requestType = request.PortInfo.RequestType;
Type? concreteType = Type.GetType($"{requestType.TypeName}, {requestType.AssemblyName}", throwOnError: false);
Type? concreteType = Type.GetType($"{requestType.TypeName}, {requestType.AssemblySimpleName}", throwOnError: false);
if (concreteType is null || !typeof(IExternalRequestEnvelope).IsAssignableFrom(concreteType))
Comment on lines +305 to 306
{
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.Checkpointing;

namespace Microsoft.Agents.AI.Workflows.UnitTests;

/// <summary>
/// Verifies that <see cref="TypeId"/> identity is independent of the assembly version, culture,
/// and public key token, so that checkpoints written by one SDK version can be restored by another.
/// See issue #6466.
/// </summary>
public class TypeIdVersionMismatchTests
{
private static string FullNameWithVersion(Type type, string version)
=> $"{type.Assembly.GetName().Name}, Version={version}, Culture=neutral, PublicKeyToken=null";

[Fact]
public void IsMatch_IgnoresAssemblyVersion()
{
Type type = typeof(TypeIdVersionMismatchTests);

// Simulate a TypeId that was serialized by a different SDK version.
TypeId legacy = new(FullNameWithVersion(type, "1.8.0.0"), type.FullName!);

legacy.IsMatch(type).Should().BeTrue();
}

[Fact]
public void Equals_IgnoresAssemblyVersion()
{
Type type = typeof(TypeIdVersionMismatchTests);

TypeId legacy = new(FullNameWithVersion(type, "1.8.0.0"), type.FullName!);
TypeId current = new(type);

legacy.Equals(current).Should().BeTrue();
(legacy == current).Should().BeTrue();
legacy.GetHashCode().Should().Be(current.GetHashCode());
}

[Fact]
public void Equals_DifferentVersions_AreEqual()
{
Type type = typeof(TypeIdVersionMismatchTests);

TypeId a = new(FullNameWithVersion(type, "1.3.0.0"), type.FullName!);
TypeId b = new(FullNameWithVersion(type, "1.10.0.0"), type.FullName!);

a.Equals(b).Should().BeTrue();
a.GetHashCode().Should().Be(b.GetHashCode());
}

[Fact]
public void DictionaryLookup_SucceedsAcrossVersionForms()
{
Type type = typeof(TypeIdVersionMismatchTests);

// Map keyed by the current (simple-name) TypeId.
Dictionary<TypeId, string> map = new()
{
[new TypeId(type)] = "value",
};

// Lookup using a legacy fully-qualified TypeId must still resolve.
TypeId legacy = new(FullNameWithVersion(type, "1.8.0.0"), type.FullName!);

map.TryGetValue(legacy, out string? value).Should().BeTrue();
value.Should().Be("value");
}

[Fact]
public void Equals_DifferentTypeName_NotEqual()
{
Type type = typeof(TypeIdVersionMismatchTests);

TypeId a = new(type);
TypeId b = new(FullNameWithVersion(type, "1.8.0.0"), typeof(TypeId).FullName!);

a.Equals(b).Should().BeFalse();
}

[Fact]
public void Equals_DifferentSimpleAssemblyName_NotEqual()
{
Type type = typeof(TypeIdVersionMismatchTests);

TypeId a = new(type);
TypeId b = new("Some.Other.Assembly, Version=1.8.0.0, Culture=neutral, PublicKeyToken=null", type.FullName!);

a.Equals(b).Should().BeFalse();
}

[Fact]
public void MalformedAssemblyName_FallsBackToRawValue()
{
// An unparseable assembly name should not throw; it should compare on the raw value.
TypeId a = new("not a valid, assembly name", "Some.Type");
TypeId b = new("not a valid, assembly name", "Some.Type");

a.Equals(b).Should().BeTrue();
}
}
Loading