// 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;
}
}