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 @@
+