From 1797adc75836046991e56d905f984ce852b48097 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Sun, 22 Jun 2025 09:38:32 -0300 Subject: [PATCH] Simplify by reusing M.E.AI AdditionalProperties No need to duplicate that code. A simplified converter does the job just as well. --- .../AdditionalPropertiesDictionary.cs | 34 --- ...AdditionalPropertiesDictionaryConverter.cs | 132 ++-------- .../AdditionalPropertiesDictionary{TValue}.cs | 232 ------------------ src/WhatsApp/Content.cs | 2 + src/WhatsApp/ConversationStorage.cs | 1 - src/WhatsApp/IMessage.cs | 2 + src/WhatsApp/JsonContext.cs | 2 + src/WhatsApp/Message.cs | 2 + src/WhatsApp/Response.cs | 6 +- src/WhatsApp/WhatsApp.csproj | 1 + 10 files changed, 39 insertions(+), 375 deletions(-) delete mode 100644 src/WhatsApp/AdditionalPropertiesDictionary.cs delete mode 100644 src/WhatsApp/AdditionalPropertiesDictionary{TValue}.cs diff --git a/src/WhatsApp/AdditionalPropertiesDictionary.cs b/src/WhatsApp/AdditionalPropertiesDictionary.cs deleted file mode 100644 index 1526733..0000000 --- a/src/WhatsApp/AdditionalPropertiesDictionary.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -using System.Text.Json.Serialization; - -namespace Devlooped.WhatsApp; - -/// Provides a dictionary used as the AdditionalProperties dictionary on Microsoft.Extensions.AI objects. -[JsonConverter(typeof(AdditionalPropertiesDictionaryConverter))] -public sealed class AdditionalPropertiesDictionary : AdditionalPropertiesDictionary -{ - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary() - { - } - - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary(IDictionary dictionary) - : base(dictionary) - { - } - - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary(IEnumerable> collection) - : base(collection) - { - } - - /// Creates a shallow clone of the properties dictionary. - /// - /// A shallow clone of the properties dictionary. The instance will not be the same as the current instance, - /// but it will contain all of the same key-value pairs. - /// - public new AdditionalPropertiesDictionary Clone() => new(this); -} \ No newline at end of file diff --git a/src/WhatsApp/AdditionalPropertiesDictionaryConverter.cs b/src/WhatsApp/AdditionalPropertiesDictionaryConverter.cs index b5b34e9..899916a 100644 --- a/src/WhatsApp/AdditionalPropertiesDictionaryConverter.cs +++ b/src/WhatsApp/AdditionalPropertiesDictionaryConverter.cs @@ -1,71 +1,34 @@ using System.Text.Json; using System.Text.Json.Serialization; - +using Microsoft.Extensions.AI; namespace Devlooped.WhatsApp; -class AdditionalPropertiesDictionaryConverter : JsonConverter +partial class AdditionalPropertiesDictionaryConverter : JsonConverter { - const string TypeKey = "$type"; - const string ValueKey = "$value"; - public override AdditionalPropertiesDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) - { throw new JsonException("Expected start of object."); - } var dictionary = new AdditionalPropertiesDictionary(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) - { return dictionary; - } if (reader.TokenType != JsonTokenType.PropertyName) - { throw new JsonException("Expected property name."); - } var key = reader.GetString()!; reader.Read(); - if (reader.TokenType == JsonTokenType.StartObject) - { - var nestedObject = JsonSerializer.Deserialize(ref reader, options); - if (nestedObject.TryGetProperty(TypeKey, out var typeElement) && typeElement.ValueKind == JsonValueKind.String) - { - var typeString = typeElement.GetString()!; - if (Enum.TryParse(typeString, out var typeCode)) - { - if (nestedObject.TryGetProperty(ValueKey, out var valueElement)) - { - var value = DeserializePrimitive(typeCode, valueElement); - dictionary[key] = value; - } - else - { - throw new JsonException($"Missing '{ValueKey}' in object with '{TypeKey}'."); - } - } - else - { - dictionary[key] = nestedObject; - } - } - else - { - dictionary[key] = nestedObject; - } - } + var value = JsonSerializer.Deserialize(ref reader, options); + if (value is JsonElement element) + dictionary[key] = GetPrimitive(element); else - { - var value = JsonSerializer.Deserialize(ref reader, options); dictionary[key] = value; - } } throw new JsonException("Unexpected end of JSON."); @@ -75,77 +38,32 @@ public override void Write(Utf8JsonWriter writer, AdditionalPropertiesDictionary { writer.WriteStartObject(); - foreach (var kvp in value) + foreach (var kvp in value.Where(x => x.Value is not null)) { writer.WritePropertyName(kvp.Key); - - if (kvp.Value == null) - { - writer.WriteNullValue(); - } - else if (IsPrimitiveType(kvp.Value.GetType())) - { - writer.WriteStartObject(); - writer.WriteString(TypeKey, GetTypeCode(kvp.Value.GetType()).ToString()); - writer.WritePropertyName(ValueKey); - JsonSerializer.Serialize(writer, kvp.Value, kvp.Value.GetType(), options); - writer.WriteEndObject(); - } - else - { - JsonSerializer.Serialize(writer, kvp.Value, kvp.Value.GetType(), options); - } + JsonSerializer.Serialize(writer, kvp.Value, options); } writer.WriteEndObject(); } - static bool IsPrimitiveType(Type type) => - type.IsPrimitive || - type == typeof(string) || - type == typeof(decimal) || - type == typeof(DateTime) || - type == typeof(Guid); - - static TypeCode GetTypeCode(Type type) => type == typeof(Guid) ? TypeCode.Object : type switch - { - var t when t == typeof(bool) => TypeCode.Boolean, - var t when t == typeof(byte) => TypeCode.Byte, - var t when t == typeof(sbyte) => TypeCode.SByte, - var t when t == typeof(char) => TypeCode.Char, - var t when t == typeof(decimal) => TypeCode.Decimal, - var t when t == typeof(double) => TypeCode.Double, - var t when t == typeof(float) => TypeCode.Single, - var t when t == typeof(int) => TypeCode.Int32, - var t when t == typeof(uint) => TypeCode.UInt32, - var t when t == typeof(long) => TypeCode.Int64, - var t when t == typeof(ulong) => TypeCode.UInt64, - var t when t == typeof(short) => TypeCode.Int16, - var t when t == typeof(ushort) => TypeCode.UInt16, - var t when t == typeof(string) => TypeCode.String, - var t when t == typeof(DateTime) => TypeCode.DateTime, - _ => throw new NotSupportedException($"Type {type} is not supported.") - }; - - static object? DeserializePrimitive(TypeCode typeCode, JsonElement element) => typeCode switch + // Helper to convert JsonElement to closest .NET primitive + static object? GetPrimitive(JsonElement element) { - TypeCode.Boolean => element.GetBoolean(), - TypeCode.Byte => element.GetByte(), - TypeCode.SByte => element.GetSByte(), - TypeCode.Char => element.GetString()![0], - TypeCode.Decimal => element.GetDecimal(), - TypeCode.Double => element.GetDouble(), - TypeCode.Single => element.GetSingle(), - TypeCode.Int32 => element.GetInt32(), - TypeCode.UInt32 => element.GetUInt32(), - TypeCode.Int64 => element.GetInt64(), - TypeCode.UInt64 => element.GetUInt64(), - TypeCode.Int16 => element.GetInt16(), - TypeCode.UInt16 => element.GetUInt16(), - TypeCode.String => element.GetString(), - TypeCode.DateTime => element.GetDateTime(), - TypeCode.Object when element.ValueKind == JsonValueKind.String => Guid.Parse(element.GetString()!), - TypeCode.Object => throw new JsonException("Expected string for Guid."), - _ => throw new NotSupportedException($"TypeCode {typeCode} is not supported.") - }; + switch (element.ValueKind) + { + case JsonValueKind.String: return element.GetString(); + case JsonValueKind.Number: + if (element.TryGetInt32(out var i)) return i; + if (element.TryGetInt64(out var l)) return l; + if (element.TryGetDouble(out var d)) return d; + return element.GetDecimal(); + case JsonValueKind.True: return true; + case JsonValueKind.False: return false; + case JsonValueKind.Null: return null; + case JsonValueKind.Object: return element; // You can recurse here if needed + case JsonValueKind.Array: return element; // Or parse as List + default: return element; + } + } } \ No newline at end of file diff --git a/src/WhatsApp/AdditionalPropertiesDictionary{TValue}.cs b/src/WhatsApp/AdditionalPropertiesDictionary{TValue}.cs deleted file mode 100644 index ee0be05..0000000 --- a/src/WhatsApp/AdditionalPropertiesDictionary{TValue}.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -namespace Devlooped.WhatsApp; - -/// Provides a dictionary used as the AdditionalProperties dictionary on Microsoft.Extensions.AI objects. -/// The type of the values in the dictionary. -[DebuggerDisplay("Count = {Count}")] -[DebuggerTypeProxy(typeof(AdditionalPropertiesDictionary<>.DebugView))] -public class AdditionalPropertiesDictionary : IDictionary, IReadOnlyDictionary -{ - /// The underlying dictionary. - readonly Dictionary dictionary; - - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary() - => dictionary = new(StringComparer.OrdinalIgnoreCase); - - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary(IDictionary dictionary) - => this.dictionary = new(dictionary, StringComparer.OrdinalIgnoreCase); - - /// Initializes a new instance of the class. - public AdditionalPropertiesDictionary(IEnumerable> collection) - => dictionary = new(collection, StringComparer.OrdinalIgnoreCase); - - /// Creates a shallow clone of the properties dictionary. - /// - /// A shallow clone of the properties dictionary. The instance will not be the same as the current instance, - /// but it will contain all of the same key-value pairs. - /// - public AdditionalPropertiesDictionary Clone() => new(dictionary); - - /// - public TValue this[string key] - { - get => dictionary[key]; - set => dictionary[key] = value; - } - - /// - public ICollection Keys => dictionary.Keys; - - /// - public ICollection Values => dictionary.Values; - - /// - public int Count => dictionary.Count; - - /// - bool ICollection>.IsReadOnly => false; - - /// - IEnumerable IReadOnlyDictionary.Keys => dictionary.Keys; - - /// - IEnumerable IReadOnlyDictionary.Values => dictionary.Values; - - /// - public void Add(string key, TValue value) => dictionary.Add(key, value); - - /// Attempts to add the specified key and value to the dictionary. - /// The key of the element to add. - /// The value of the element to add. - /// if the key/value pair was added to the dictionary successfully; otherwise, . - public bool TryAdd(string key, TValue value) => dictionary.TryAdd(key, value); - - /// - void ICollection>.Add(KeyValuePair item) => ((ICollection>)dictionary).Add(item); - - /// - public void Clear() => dictionary.Clear(); - - /// - bool ICollection>.Contains(KeyValuePair item) => - ((ICollection>)dictionary).Contains(item); - - /// - public bool ContainsKey(string key) => dictionary.ContainsKey(key); - - /// - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => - ((ICollection>)dictionary).CopyTo(array, arrayIndex); - - /// - /// Returns an enumerator that iterates through the . - /// - /// An that enumerates the contents of the . - public Enumerator GetEnumerator() => new(dictionary.GetEnumerator()); - - /// - IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - public bool Remove(string key) => dictionary.Remove(key); - - /// - bool ICollection>.Remove(KeyValuePair item) => ((ICollection>)dictionary).Remove(item); - - /// Attempts to extract a typed value from the dictionary. - /// The type of the value to be retrieved. - /// The key to locate. - /// - /// When this method returns, contains the value retrieved from the dictionary, if found and successfully converted to the requested type; - /// otherwise, the default value of . - /// - /// - /// if a non- value was found for - /// in the dictionary and converted to the requested type; otherwise, . - /// - /// - /// If a non- value is found for the key in the dictionary, but the value is not of the requested type and is - /// an object, the method attempts to convert the object to the requested type. - /// - public bool TryGetValue(string key, [NotNullWhen(true)] out T? value) - { - if (TryGetValue(key, out TValue? obj)) - { - switch (obj) - { - case T t: - // The object is already of the requested type. Return it. - value = t; - return true; - - case IConvertible: - // The object is convertible; try to convert it to the requested type. Unfortunately, there's no - // convenient way to do this that avoids exceptions and that doesn't involve a ton of boilerplate, - // so we only try when the source object is at least an IConvertible, which is what ChangeType uses. - try - { - value = (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); - return true; - } - catch (Exception e) when (e is ArgumentException or FormatException or InvalidCastException or OverflowException) - { - // Ignore known failure modes. - } - - break; - } - } - - // Unable to find the value or convert it to the requested type. - value = default; - return false; - } - - /// Gets the value associated with the specified key. - /// if the contains an element with the specified key; otherwise . - public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) => dictionary.TryGetValue(key, out value); - - /// - bool IDictionary.TryGetValue(string key, out TValue value) => dictionary.TryGetValue(key, out value!); - - /// - bool IReadOnlyDictionary.TryGetValue(string key, out TValue value) => dictionary.TryGetValue(key, out value!); - - /// Copies all of the entries from into the dictionary, overwriting any existing items in the dictionary with the same key. - /// The items to add. - internal void SetAll(IEnumerable> items) - { - _ = Throw.IfNull(items); - - foreach (var item in items) - { - dictionary[item.Key] = item.Value; - } - } - - /// Enumerates the elements of an . - public struct Enumerator : IEnumerator> - { - /// The wrapped dictionary enumerator. - Dictionary.Enumerator _dictionaryEnumerator; - - /// Initializes a new instance of the struct with the dictionary enumerator to wrap. - /// The dictionary enumerator to wrap. - internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) - { - _dictionaryEnumerator = dictionaryEnumerator; - } - - /// - public KeyValuePair Current => _dictionaryEnumerator.Current; - - /// - object IEnumerator.Current => Current; - - /// - public void Dispose() => _dictionaryEnumerator.Dispose(); - - /// - public bool MoveNext() => _dictionaryEnumerator.MoveNext(); - - /// - public void Reset() => Reset(ref _dictionaryEnumerator); - - /// Calls on an enumerator. - static void Reset(ref TEnumerator enumerator) - where TEnumerator : struct, IEnumerator - { - enumerator.Reset(); - } - } - - /// Provides a debugger view for the collection. - sealed class DebugView(AdditionalPropertiesDictionary properties) - { - readonly AdditionalPropertiesDictionary _properties = Throw.IfNull(properties); - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public AdditionalProperty[] Items => (from p in _properties select new AdditionalProperty(p.Key, p.Value)).ToArray(); - - [DebuggerDisplay("{Value}", Name = "[{Key}]")] - public readonly struct AdditionalProperty(string key, TValue value) - { - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public string Key { get; } = key; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public TValue Value { get; } = value; - } - } -} \ No newline at end of file diff --git a/src/WhatsApp/Content.cs b/src/WhatsApp/Content.cs index f21e98e..42ea334 100644 --- a/src/WhatsApp/Content.cs +++ b/src/WhatsApp/Content.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; namespace Devlooped.WhatsApp; @@ -18,6 +19,7 @@ namespace Devlooped.WhatsApp; public abstract record Content { /// Gets or sets any additional properties associated with the content. + [JsonConverter(typeof(AdditionalPropertiesDictionaryConverter))] public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// diff --git a/src/WhatsApp/ConversationStorage.cs b/src/WhatsApp/ConversationStorage.cs index c222adb..8f3ad81 100644 --- a/src/WhatsApp/ConversationStorage.cs +++ b/src/WhatsApp/ConversationStorage.cs @@ -33,7 +33,6 @@ protected virtual IDocumentRepository CreateActiveConversationRepo /// public async Task SaveAsync(IMessage message, CancellationToken cancellationToken = default) { - var data = defaultSerializer.Serialize(message); if (!string.IsNullOrEmpty(message.ConversationId)) { var conversation = await conversationsRepository.Value.GetAsync(message.UserNumber, message.ConversationId, cancellationToken) ?? diff --git a/src/WhatsApp/IMessage.cs b/src/WhatsApp/IMessage.cs index 9d258f4..148f3c5 100644 --- a/src/WhatsApp/IMessage.cs +++ b/src/WhatsApp/IMessage.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; namespace Devlooped.WhatsApp; @@ -22,6 +23,7 @@ namespace Devlooped.WhatsApp; public interface IMessage { /// Gets or sets any additional properties associated with the message. + [JsonConverter(typeof(AdditionalPropertiesDictionaryConverter))] AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// diff --git a/src/WhatsApp/JsonContext.cs b/src/WhatsApp/JsonContext.cs index cc948c7..acc9428 100644 --- a/src/WhatsApp/JsonContext.cs +++ b/src/WhatsApp/JsonContext.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.AI; namespace Devlooped.WhatsApp; @@ -38,6 +39,7 @@ namespace Devlooped.WhatsApp; [JsonSerializable(typeof(UnsupportedMessage))] [JsonSerializable(typeof(MediaReference))] [JsonSerializable(typeof(Conversation))] +[JsonSerializable(typeof(AdditionalPropertiesDictionary))] public partial class JsonContext : JsonSerializerContext { static readonly Lazy options = new(() => CreateDefaultOptions()); diff --git a/src/WhatsApp/Message.cs b/src/WhatsApp/Message.cs index 022cb7e..b006008 100644 --- a/src/WhatsApp/Message.cs +++ b/src/WhatsApp/Message.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; namespace Devlooped.WhatsApp; @@ -20,6 +21,7 @@ namespace Devlooped.WhatsApp; public abstract partial record Message(string Id, Service Service, User User, long Timestamp) : IMessage { /// + [JsonConverter(typeof(AdditionalPropertiesDictionaryConverter))] public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// diff --git a/src/WhatsApp/Response.cs b/src/WhatsApp/Response.cs index 63deefb..8ea745a 100644 --- a/src/WhatsApp/Response.cs +++ b/src/WhatsApp/Response.cs @@ -1,4 +1,7 @@ -namespace Devlooped.WhatsApp; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Devlooped.WhatsApp; /// /// Represents a response message or command that can be sent using a WhatsApp client. @@ -14,6 +17,7 @@ public abstract partial record Response(string UserNumber, string ServiceId, string Context, string? ConversationId) : IMessage { /// + [JsonConverter(typeof(AdditionalPropertiesDictionaryConverter))] public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } /// diff --git a/src/WhatsApp/WhatsApp.csproj b/src/WhatsApp/WhatsApp.csproj index 84e0cdf..8b6bfcb 100644 --- a/src/WhatsApp/WhatsApp.csproj +++ b/src/WhatsApp/WhatsApp.csproj @@ -14,6 +14,7 @@ +