// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace Microsoft.Teams.Core.Schema; /// /// Represents a dictionary for storing extended properties as key-value pairs. /// public class ExtendedPropertiesDictionary : Dictionary { /// /// Initializes a new empty instance of the class. /// public ExtendedPropertiesDictionary() { } /// /// Initializes a new instance of the class by shallow-copying entries from another dictionary. /// public ExtendedPropertiesDictionary(IDictionary source) : base(source) { } /// /// Extracts and deserializes a value from the dictionary, removing the entry if found. /// Returns the deserialized value, or default if the key is not present. /// public T? Extract(string key) { if (!TryGetValue(key, out object? raw)) return default; Remove(key); if (raw is T typed) return typed; if (raw is System.Text.Json.JsonElement element) return System.Text.Json.JsonSerializer.Deserialize(element.GetRawText()); return default; } /// /// Gets and deserializes a value from the dictionary without removing it. /// Handles values that result from deserialization. /// public T? Get(string key) { if (!TryGetValue(key, out object? raw)) return default; if (raw is T typed) return typed; if (raw is System.Text.Json.JsonElement element) { T? deserialized = System.Text.Json.JsonSerializer.Deserialize(element.GetRawText()); this[key] = deserialized; return deserialized; } return default; } } /// /// Represents a core activity object that encapsulates the data and metadata for a bot interaction. /// /// /// This class provides the foundational structure for bot activities including message exchanges, /// conversation updates, and other bot-related events. It supports serialization to and from JSON /// and includes extension properties for channel-specific data. /// Follows the Activity Protocol Specification: https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md /// public class CoreActivity { /// /// Gets or sets the type of the activity. See for common values. /// /// /// Common activity types include "message", "conversationUpdate", "contactRelationUpdate", etc. /// [JsonPropertyName("type")] public string Type { get; set; } /// /// Gets or sets the unique identifier for the channel on which this activity is occurring. /// [JsonPropertyName("channelId")] public string? ChannelId { get; set; } /// /// Gets or sets the unique identifier for the activity. /// [JsonPropertyName("id")] public string? Id { get; set; } /// /// Gets or sets the URL of the service endpoint for this activity. /// /// /// This URL is used to send responses back to the channel. /// [JsonPropertyName("serviceUrl")] public Uri? ServiceUrl { get; set; } /// /// Gets or sets the identifier of the activity this activity is a reply to. /// [JsonPropertyName("replyToId")] public string? ReplyToId { get; set; } /// /// Gets or sets the conversation information for this activity. /// [JsonPropertyName("conversation")] public Conversation? Conversation { get; set; } /// /// Gets or sets the sender account for this activity. /// [JsonPropertyName("from")] public ChannelAccount? From { get; set; } /// /// Gets or sets the recipient account for this activity. /// [JsonPropertyName("recipient")] public ChannelAccount? Recipient { get; set; } /// /// Gets the extension data dictionary for storing additional properties not defined in the schema. /// [JsonExtensionData] public ExtendedPropertiesDictionary Properties { get; set; } = []; /// /// Gets the JSON serializer options used for reflection-based serialization of extended activity types. /// /// /// Uses reflection-based serialization to support custom activity types that extend CoreActivity. /// This is used when serializing/deserializing types not registered in the source-generated context. /// private static readonly JsonSerializerOptions ReflectionJsonOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// /// Creates a new instance of the class with the specified activity type. /// Defaults to . /// /// The activity type. Defaults to "message". [JsonConstructor] public CoreActivity(string type = ActivityType.Message) { Type = type; } /// /// Creates a new instance of the class by copying properties from another activity. /// /// The source activity to copy from. protected CoreActivity(CoreActivity activity) { ArgumentNullException.ThrowIfNull(activity); Id = activity.Id; ServiceUrl = activity.ServiceUrl; ChannelId = activity.ChannelId; Type = activity.Type; Conversation = activity.Conversation is not null ? new Conversation(activity.Conversation.Id) { Properties = new ExtendedPropertiesDictionary(activity.Conversation.Properties) } : null; From = activity.From is not null ? CloneChannelAccount(activity.From) : null; Recipient = activity.Recipient is not null ? CloneChannelAccount(activity.Recipient) : null; Properties = new ExtendedPropertiesDictionary(activity.Properties); } #pragma warning disable ExperimentalTeamsTargeted private static ChannelAccount CloneChannelAccount(ChannelAccount source) => new() { Id = source.Id, Name = source.Name, IsTargeted = source.IsTargeted, AgenticAppId = source.AgenticAppId, AgenticUserId = source.AgenticUserId, AgenticAppBlueprintId = source.AgenticAppBlueprintId, Properties = new ExtendedPropertiesDictionary(source.Properties) }; #pragma warning restore ExperimentalTeamsTargeted /// /// Serializes the current activity to a JSON string. /// /// A JSON string representation of the activity. public virtual string ToJson() => JsonSerializer.Serialize(this, CoreActivityJsonContext.Default.CoreActivity); /// /// Serializes the current activity to a JSON string using the specified for source-generated serialization. /// /// The type of the activity to serialize. Must inherit from . /// The JSON type info that provides serialization metadata for type . /// A JSON string representation of the activity. public string ToJson(JsonTypeInfo jsonTypeInfo) where T : CoreActivity => JsonSerializer.Serialize(this, jsonTypeInfo); /// /// Serializes the specified activity instance to a JSON string using reflection-based serialization. /// /// Uses reflection-based serialization to support custom activity types that extend /// . The resulting JSON reflects the public properties of the activity instance. /// The type of the activity to serialize. Must inherit from CoreActivity. /// The activity instance to serialize. Cannot be null. /// A JSON string representation of the specified activity instance. public static string ToJson(T instance) where T : CoreActivity => JsonSerializer.Serialize(instance, ReflectionJsonOptions); /// /// Deserializes a JSON string into a object. /// /// The JSON string to deserialize. /// A instance. public static CoreActivity FromJsonString(string json) => JsonSerializer.Deserialize(json, CoreActivityJsonContext.Default.CoreActivity)!; /// /// Asynchronously deserializes a JSON stream into a object. /// /// The stream containing JSON data to deserialize. /// A cancellation token to cancel the operation. /// A task that represents the asynchronous operation. The task result contains the deserialized instance, or null if deserialization fails. public static ValueTask FromJsonStreamAsync(Stream stream, CancellationToken cancellationToken = default) => JsonSerializer.DeserializeAsync(stream, CoreActivityJsonContext.Default.CoreActivity, cancellationToken); /// /// Asynchronously deserializes a JSON stream into an instance of type using the specified for source-generated serialization. /// /// The type of the activity to deserialize. Must inherit from . /// The stream containing JSON data to deserialize. /// The JSON type info that provides deserialization metadata for type . /// A cancellation token to cancel the operation. /// A representing the asynchronous operation. The result contains the deserialized activity, or null if deserialization fails. public static ValueTask FromJsonStreamAsync(Stream stream, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) where T : CoreActivity => JsonSerializer.DeserializeAsync(stream, jsonTypeInfo, cancellationToken); /// /// Asynchronously deserializes a JSON value from the specified stream into an instance of type T. /// /// The caller is responsible for managing the lifetime of the provided stream. The method uses /// default JSON serialization options. /// The type of the object to deserialize. Must derive from CoreActivity. /// The stream containing the JSON data to deserialize. The stream must be readable and positioned at the start of /// the JSON content. /// A cancellation token that can be used to cancel the asynchronous operation. /// A ValueTask that represents the asynchronous operation. The result contains an instance of type T if /// deserialization is successful; otherwise, null. public static ValueTask FromJsonStreamAsync(Stream stream, CancellationToken cancellationToken = default) where T : CoreActivity => JsonSerializer.DeserializeAsync(stream, ReflectionJsonOptions, cancellationToken); /// /// Creates a new instance of the to construct activity instances. /// /// A new instance. public static CoreActivityBuilder CreateBuilder() => new(); }