// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.Agents.AI.Workflows.Checkpointing; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows; /// /// Represents a value that can be exported / imported to a workflow, e.g. through an external request/response, or /// through checkpointing. Abstracts away delayed deserialization and type conversion where appropriate. /// public sealed class PortableValue { /// /// Initializes a new instance . /// /// The represented value. public PortableValue(object value) { this._value = value; this.TypeId = new(value.GetType()); } [JsonConstructor] internal PortableValue(TypeId typeId, object value) { this.TypeId = Throw.IfNull(typeId); this._value = value; } /// public override bool Equals(object? obj) { if (obj is null) { return false; } if (obj is not PortableValue other) { Type targetType = obj.GetType(); return this.AsType(targetType)?.Equals(obj) is true; } return this.TypeId == other.TypeId && ((this.Value is null && other.Value is null) || this.Value?.Equals(other.Value) is true); } /// public override int GetHashCode() { return HashCode.Combine(this.TypeId, this.Value); } /// public static bool operator ==(PortableValue? left, PortableValue? right) { if (left is null) { return right is null; } return left.Equals(right); } /// public static bool operator !=(PortableValue? left, PortableValue? right) => !(left == right); /// /// The identifier of the type of the instance in . /// public TypeId TypeId { get; } [JsonIgnore] internal bool IsDelayedDeserialization => this.Value is IDelayedDeserialization; [JsonIgnore] internal bool IsDeserialized => this._deserializedValueCache is not null; private readonly object _value; private object? _deserializedValueCache; /// /// Gets the raw underlying value represented by this instance. /// [JsonInclude] internal object Value => this._deserializedValueCache ?? Throw.IfNull(this._value); /// /// Attempts to retrieve the underlying value as the specified type, deserializing if necessary. /// /// If the underlying value implements delayed deserialization, this method will attempt to /// deserialize it to the specified type. If the value is already of the requested type, it is returned directly. /// Otherwise, the default value for TValue is returned. For value types, the default is not , /// UNLESS is nullable, e.g. int?. /// /// The type to which the value should be cast or deserialized. /// The value cast or deserialized to type TValue if possible; otherwise, the default value for type TValue. public TValue? As() => this.Is(out TValue? value) ? value : default; /// /// Determines whether the current value can be represented as the specified type. /// /// The type to test for compatibility with the current value. /// true if the current value can be represented as type TValue; otherwise, false. public bool Is() => this.Is(out _); /// /// Determines whether the current value can be represented as the specified type. /// /// The type to test for compatibility with the current value. /// When this method returns, contains the value cast or deserialized to type TValue /// if the conversion succeeded, or null if the conversion failed. /// true if the current value can be represented as type TValue; otherwise, false. public bool Is([NotNullWhen(true)] out TValue? value) { this.TryDeserializeAndUpdateCache(typeof(TValue), out _); if (this.Value is TValue typedValue) { value = typedValue; return true; } value = default; return false; } /// /// Attempts to retrieve the underlying value as the specified type, deserializing if necessary. /// /// The type to which the value should be cast or deserialized. /// The value cast or deserialized to type targetType if possible; otherwise, null. public object? AsType(Type targetType) => this.IsType(targetType, out object? value) ? value : null; /// /// Determines whether the current instance can be assigned to the specified target type. /// /// The type to compare with the current instance. Cannot be null. /// true if the current instance can be assigned to targetType; otherwise, false. public bool IsType(Type targetType) => this.IsType(targetType, out _); /// /// Determines whether the current instance can be assigned to the specified target type. /// /// The type to compare with the current instance. Cannot be null. /// When this method returns, contains the value cast or deserialized to type TValue /// if the conversion succeeded, or null if the conversion failed. /// true if the current instance can be assigned to targetType; otherwise, false. public bool IsType(Type targetType, [NotNullWhen(true)] out object? value) { // Unfortunately, there is no way to check that the TypeId specified is assignable to the provided type Throw.IfNull(targetType); this.TryDeserializeAndUpdateCache(targetType, out _); if (this.Value is not null && targetType.IsInstanceOfType(this.Value)) { value = this.Value; return true; } value = null; return false; } private bool TryDeserializeAndUpdateCache(Type targetType, out object? replacedCacheValueOrNull) { replacedCacheValueOrNull = null; // Explicitly use _value here since we do not want to be overridden by the cache, if any if (this._value is not IDelayedDeserialization delayedDeserialization) { // Not a delayed deserialization; nothing to do return false; } bool isCompatibleType = false; if (this._deserializedValueCache == null || !(isCompatibleType = targetType.IsAssignableFrom(this._deserializedValueCache.GetType()))) { // Either we have no cache, or the types are incompatible; see if we can deserialize try { object? deserialized = delayedDeserialization.Deserialize(targetType); if (deserialized != null && targetType.IsInstanceOfType(deserialized)) { replacedCacheValueOrNull = this._deserializedValueCache; this._deserializedValueCache = deserialized; return true; } } catch { isCompatibleType = false; } } // The last possibility is that we already deserialized successfully return isCompatibleType; } }