diff --git a/Kook.Net.slnx b/Kook.Net.slnx index 43122329..3dd2fb36 100644 --- a/Kook.Net.slnx +++ b/Kook.Net.slnx @@ -19,6 +19,7 @@ + diff --git a/props/common.props b/props/common.props index fe9aa6eb..b83f1109 100644 --- a/props/common.props +++ b/props/common.props @@ -5,8 +5,6 @@ 0.10.4 false - false - false latest true snupkg @@ -31,6 +29,11 @@ true pdbonly + + + true + true + $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) diff --git a/samples/Kook.Net.Samples.NativeAOT/Kook.Net.Samples.NativeAOT.csproj b/samples/Kook.Net.Samples.NativeAOT/Kook.Net.Samples.NativeAOT.csproj new file mode 100644 index 00000000..e1581827 --- /dev/null +++ b/samples/Kook.Net.Samples.NativeAOT/Kook.Net.Samples.NativeAOT.csproj @@ -0,0 +1,20 @@ + + + + + + win-x64 + true + false + true + true + full + Speed + false + + + + + + + diff --git a/samples/Kook.Net.Samples.NativeAOT/Program.cs b/samples/Kook.Net.Samples.NativeAOT/Program.cs new file mode 100644 index 00000000..a4c368e0 --- /dev/null +++ b/samples/Kook.Net.Samples.NativeAOT/Program.cs @@ -0,0 +1,86 @@ +using Kook; +using Kook.WebSocket; + +// Kook.Net NativeAOT 示例 +// 此示例演示如何在 NativeAOT 编译模式下使用 Kook.Net + +Console.WriteLine("Kook.Net NativeAOT Sample"); +Console.WriteLine("========================"); +Console.WriteLine(); + +// 从环境变量或配置文件读取 Token +string? token = Environment.GetEnvironmentVariable("KOOK_TOKEN"); +if (string.IsNullOrEmpty(token)) +{ + Console.WriteLine("Error: KOOK_TOKEN environment variable is not set."); + Console.WriteLine("Please set your bot token:"); + Console.WriteLine(" export KOOK_TOKEN=\"your-bot-token-here\""); + return 1; +} + +// 创建客户端配置 +// 注意:NativeAOT 不支持 Kook.Net.Commands 框架,因为它依赖反射 +KookSocketConfig config = new() +{ + AlwaysDownloadUsers = false, + MessageCacheSize = 100, + LogLevel = LogSeverity.Info, + StartupCacheFetchMode = StartupCacheFetchMode.Synchronous +}; + +// 创建客户端 +using KookSocketClient client = new(config); + +// 设置事件处理器 +client.Log += LogAsync; +client.Ready += ReadyAsync; +client.MessageReceived += MessageReceivedAsync; + +// 登录并启动 +try +{ + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + Console.WriteLine("Bot is running. Press Ctrl+C to exit."); + + // 保持程序运行 + await Task.Delay(Timeout.Infinite); +} +catch (Exception ex) +{ + Console.WriteLine($"Error: {ex.Message}"); + return 1; +} + +return 0; + +static Task LogAsync(LogMessage msg) +{ + Console.WriteLine($"[{msg.Severity}] {msg.Source}: {msg.Message}"); + if (msg.Exception != null) + Console.WriteLine($" Exception: {msg.Exception}"); + return Task.CompletedTask; +} + +static Task ReadyAsync() +{ + Console.WriteLine("Bot is ready!"); + return Task.CompletedTask; +} + +static Task MessageReceivedAsync(SocketMessage message, SocketGuildUser user, SocketTextChannel channel) +{ + // 忽略系统消息和 Bot 自己的消息 + if (message.Author.IsBot == true || message.Author.IsSystemUser) + return Task.CompletedTask; + + // 简单的 ping 命令 + if (message.Content.Equals("!ping", StringComparison.OrdinalIgnoreCase)) + { + // 在 NativeAOT 模式下,直接回复消息 + _ = message.Channel.SendTextAsync("Pong! 🏓"); + } + + return Task.CompletedTask; +} diff --git a/samples/Kook.Net.Samples.NativeAOT/README.md b/samples/Kook.Net.Samples.NativeAOT/README.md new file mode 100644 index 00000000..ab162a05 --- /dev/null +++ b/samples/Kook.Net.Samples.NativeAOT/README.md @@ -0,0 +1,59 @@ +# Kook.Net NativeAOT 示例 + +此示例演示如何在 NativeAOT 编译模式下使用 Kook.Net。 + +## 功能特性 + +- ✅ REST API 调用(使用 JSON 源生成) +- ✅ WebSocket 连接 +- ✅ 事件处理 +- ✅ 消息发送和接收 +- ❌ Commands 框架(不支持,因为依赖反射) + +## 运行示例 + +### 开发模式运行 + +```bash +export KOOK_TOKEN="your-bot-token-here" +dotnet run +``` + +### 发布为 NativeAOT + +```bash +dotnet publish -c Release + +# 运行已编译的原生可执行文件 +export KOOK_TOKEN="your-bot-token-here" +./bin/Release/net8.0/linux-x64/publish/Kook.Net.Samples.NativeAOT +``` + +## NativeAOT 限制 + +1. **Commands 框架不可用**: `Kook.Net.Commands` 使用反射进行命令发现和参数绑定,这与 NativeAOT 不兼容。 + - 解决方案:手动处理消息并实现命令逻辑 + +2. **JSON 序列化**: Kook.Net 使用源生成的 `KookJsonSerializerContext` 来支持 NativeAOT。 + - 所有 API 模型都已注册用于源生成 + - 自定义类型可能需要额外的序列化配置 + +3. **程序集大小**: NativeAOT 编译的可执行文件会比常规 .NET 应用程序大,但启动速度更快,内存占用更低。 + +## 测试 Bot + +运行 Bot 后,在 Kook 频道中发送 `!ping` 命令,Bot 会回复 "Pong! 🏓"。 + +## 性能优势 + +NativeAOT 编译的应用程序具有以下优势: + +- **快速启动**: 无需 JIT 编译 +- **较低内存占用**: 无需加载整个 .NET 运行时 +- **单文件部署**: 可执行文件包含所有依赖项 +- **更好的代码保护**: 原生代码更难反编译 + +## 相关资源 + +- [.NET NativeAOT 文档](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) +- [Kook.Net 文档](https://kooknet.dev/) diff --git a/samples/Kook.Net.Samples.SimpleBot/Program.cs b/samples/Kook.Net.Samples.SimpleBot/Program.cs index cb26d750..a3fafadc 100644 --- a/samples/Kook.Net.Samples.SimpleBot/Program.cs +++ b/samples/Kook.Net.Samples.SimpleBot/Program.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Text.Json; using Kook; using Kook.Rest; using Kook.WebSocket; diff --git a/src/Kook.Net.Commands/Kook.Net.Commands.csproj b/src/Kook.Net.Commands/Kook.Net.Commands.csproj index 5503dfd7..83b8ce35 100644 --- a/src/Kook.Net.Commands/Kook.Net.Commands.csproj +++ b/src/Kook.Net.Commands/Kook.Net.Commands.csproj @@ -5,6 +5,9 @@ true Kook.Net.Commands The text message based command framework extension for Kook.Net. + + false + false diff --git a/src/Kook.Net.Commands/Readers/NullableTypeReader.cs b/src/Kook.Net.Commands/Readers/NullableTypeReader.cs index 949e85ce..d8ed179c 100644 --- a/src/Kook.Net.Commands/Readers/NullableTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/NullableTypeReader.cs @@ -1,16 +1,50 @@ +using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Kook.Commands; internal static class NullableTypeReader { + private static readonly FrozenDictionary> PrimitiveNullableReaders = + new Dictionary> + { + [typeof(bool)] = x => new NullableTypeReader(x), + [typeof(byte)] = x => new NullableTypeReader(x), + [typeof(sbyte)] = x => new NullableTypeReader(x), + [typeof(short)] = x => new NullableTypeReader(x), + [typeof(ushort)] = x => new NullableTypeReader(x), + [typeof(int)] = x => new NullableTypeReader(x), + [typeof(uint)] = x => new NullableTypeReader(x), + [typeof(long)] = x => new NullableTypeReader(x), + [typeof(ulong)] = x => new NullableTypeReader(x), + [typeof(float)] = x => new NullableTypeReader(x), + [typeof(double)] = x => new NullableTypeReader(x), + [typeof(decimal)] = x => new NullableTypeReader(x), + [typeof(DateTime)] = x => new NullableTypeReader(x), + [typeof(DateTimeOffset)] = x => new NullableTypeReader(x), + [typeof(Guid)] = x => new NullableTypeReader(x), + [typeof(TimeSpan)] = x => new NullableTypeReader(x), +#if NET6_0_OR_GREATER + [typeof(DateOnly)] = x => new NullableTypeReader(x), + [typeof(TimeOnly)] = x => new NullableTypeReader(x), +#endif + [typeof(char)] = x => new NullableTypeReader(x), + } + .ToFrozenDictionary(); + public static TypeReader Create(Type type, TypeReader reader) { + if (PrimitiveNullableReaders.TryGetValue(type, out Func? factory)) + return factory(reader); ConstructorInfo constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First(); return (TypeReader)constructor.Invoke([reader]); } } +#if NET6_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif internal class NullableTypeReader : TypeReader where T : struct { @@ -26,7 +60,7 @@ public override async Task ReadAsync(ICommandContext context, { if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase)) - return TypeReaderResult.FromSuccess(new T()); + return TypeReaderResult.FromSuccess(new T( )); return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false); } } diff --git a/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs index fe42a25f..c14db4f2 100644 --- a/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Kook.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -1,14 +1,48 @@ +using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; + namespace Kook.Commands; internal static class PrimitiveTypeReader { + private static readonly FrozenDictionary> PrimitiveReaders = + new Dictionary> + { + [typeof(bool)] = () => new PrimitiveTypeReader(), + [typeof(byte)] = () => new PrimitiveTypeReader(), + [typeof(sbyte)] = () => new PrimitiveTypeReader(), + [typeof(short)] = () => new PrimitiveTypeReader(), + [typeof(ushort)] = () => new PrimitiveTypeReader(), + [typeof(int)] = () => new PrimitiveTypeReader(), + [typeof(uint)] = () => new PrimitiveTypeReader(), + [typeof(long)] = () => new PrimitiveTypeReader(), + [typeof(ulong)] = () => new PrimitiveTypeReader(), + [typeof(float)] = () => new PrimitiveTypeReader(), + [typeof(double)] = () => new PrimitiveTypeReader(), + [typeof(decimal)] = () => new PrimitiveTypeReader(), + [typeof(DateTime)] = () => new PrimitiveTypeReader(), + [typeof(DateTimeOffset)] = () => new PrimitiveTypeReader(), + [typeof(Guid)] = () => new PrimitiveTypeReader(), +#if NET6_0_OR_GREATER + [typeof(DateOnly)] = () => new PrimitiveTypeReader(), + [typeof(TimeOnly)] = () => new PrimitiveTypeReader(), +#endif + [typeof(char)] = () => new PrimitiveTypeReader(), + } + .ToFrozenDictionary(); + public static TypeReader? Create(Type type) { + if (PrimitiveReaders.TryGetValue(type, out Func? factory)) + return factory(); type = typeof(PrimitiveTypeReader<>).MakeGenericType(type); return Activator.CreateInstance(type) as TypeReader; } } +#if NET6_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif internal class PrimitiveTypeReader : TypeReader { private readonly TryParseDelegate _tryParse; diff --git a/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs b/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs index ca1688ef..452a856e 100644 --- a/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs +++ b/src/Kook.Net.Core/Entities/Guilds/GuildFeatures.cs @@ -61,7 +61,11 @@ internal void EnsureFeature(GuildFeature feature) { if (HasFeature(feature)) return; GuildFeatures features = this; +#if NET8_0_OR_GREATER + IEnumerable values = Enum.GetValues(); +#else IEnumerable values = Enum.GetValues(typeof(GuildFeature)).Cast(); +#endif IEnumerable missingValues = values.Where(x => feature.HasFlag(x) && !features.Value.HasFlag(x)).ToList(); throw new InvalidOperationException($"Missing required guild feature{(missingValues.Count() > 1 ? "s" : "")} {string.Join(", ", missingValues.Select(x => x.ToString()))} in order to execute this operation."); } diff --git a/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs b/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs index b5893aee..627b16f9 100644 --- a/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs +++ b/src/Kook.Net.Core/Entities/Messages/Embeds/NotImplementedEmbed.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace Kook; @@ -35,6 +37,10 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode) /// 用于反序列化操作的选项。 /// 要解析为的具体类型。 /// 解析后的嵌入式内容。 +#if NET5_0_OR_GREATER + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a resolving function for AOT applications.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a resolving function for AOT applications.")] +#endif public T? Resolve(JsonSerializerOptions? options = null) where T : IEmbed { @@ -47,6 +53,34 @@ internal NotImplementedEmbed(string rawType, JsonNode jsonNode) return embed; } + /// + /// 通过 JSON 反序列化将嵌入式内容解析为具体类型。 + /// + /// 用于反序列化操作的类型信息。 + /// 要解析为的具体类型。 + /// 解析后的嵌入式内容。 + public T? Resolve(JsonTypeInfo jsonTypeInfo) + where T : IEmbed + { + if (jsonTypeInfo is not JsonTypeInfo typedJsonTypeInfo) + throw new ArgumentException($"The provided JsonTypeInfo is not of the expected type {typeof(T).FullName}.", nameof(jsonTypeInfo)); + T? embed = JsonNode.Deserialize(typedJsonTypeInfo); + return embed; + } + + /// + /// 通过 JSON 反序列化将嵌入式内容解析为具体类型。 + /// + /// 用于反序列化操作的类型信息。 + /// 要解析为的具体类型。 + /// 解析后的嵌入式内容。 + public T? Resolve(JsonTypeInfo jsonTypeInfo) + where T : IEmbed + { + T? embed = JsonNode.Deserialize(jsonTypeInfo); + return embed; + } + /// /// 通过指定的解析函数将嵌入式内容解析为具体类型。 /// diff --git a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs index 649db085..fda728e4 100644 --- a/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs +++ b/src/Kook.Net.Core/Entities/Messages/Pokes/PokeResources/NotImplementedPokeResource.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace Kook; @@ -35,6 +37,10 @@ internal NotImplementedPokeResource(string rawType, JsonNode jsonNode) /// 用于反序列化操作的选项。 /// 要解析为的具体类型。 /// 解析后的 POKE 资源。 +#if NET5_0_OR_GREATER + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a resolving function for AOT applications.")] + [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use the overload that takes a resolving function for AOT applications.")] +#endif public T? Resolve(JsonSerializerOptions? options = null) where T : IPokeResource { @@ -47,6 +53,34 @@ internal NotImplementedPokeResource(string rawType, JsonNode jsonNode) return pokeResource; } + /// + /// 通过 JSON 反序列化将 POKE 资源解析为具体类型。 + /// + /// 用于反序列化操作的类型信息。 + /// 要解析为的具体类型。 + /// 解析后的 POKE 资源。 + public T? Resolve(JsonTypeInfo jsonTypeInfo) + where T : IPokeResource + { + if (jsonTypeInfo is not JsonTypeInfo typedJsonTypeInfo) + throw new ArgumentException($"The provided JsonTypeInfo is not of the expected type {typeof(T).FullName}.", nameof(jsonTypeInfo)); + T? pokeResource = JsonNode.Deserialize(typedJsonTypeInfo); + return pokeResource; + } + + /// + /// 通过 JSON 反序列化将 POKE 资源解析为具体类型。 + /// + /// 用于反序列化操作的类型信息。 + /// 要解析为的具体类型。 + /// 解析后的 POKE 资源。 + public T? Resolve(JsonTypeInfo jsonTypeInfo) + where T : IPokeResource + { + T? pokeResource = JsonNode.Deserialize(jsonTypeInfo); + return pokeResource; + } + /// /// 通过指定的解析函数将 POKE 资源 解析为具体类型。 /// diff --git a/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs index 594c7e17..81500ecf 100644 --- a/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Kook.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -435,9 +435,13 @@ internal void Ensure(GuildPermission permissions) { if (!Has(permissions)) { +#if NET8_0_OR_GREATER + IEnumerable vals = Enum.GetValues(); +#else IEnumerable vals = Enum .GetValues(typeof(GuildPermission)) .Cast(); +#endif ulong currentValues = RawValue; IEnumerable missingValues = vals .Where(x => permissions.HasFlag(x) && !Permissions.GetValue(currentValues, x)) diff --git a/src/Kook.Net.Core/Utils/Cacheable.cs b/src/Kook.Net.Core/Utils/Cacheable.cs index 3e145e6a..05e62b46 100644 --- a/src/Kook.Net.Core/Utils/Cacheable.cs +++ b/src/Kook.Net.Core/Utils/Cacheable.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; namespace Kook; @@ -9,9 +7,6 @@ namespace Kook; /// /// 可延迟加载的缓存实体的类型。 /// 可延迟加载的缓存实体的 ID 的类型。 -#if DEBUG -[DebuggerDisplay("{DebuggerDisplay,nq}")] -#endif public readonly struct Cacheable where TEntity : IEntity where TId : IEquatable @@ -70,11 +65,6 @@ public Cacheable(TEntity? value, TId id, bool hasValue, Func> dow /// public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); -#if DEBUG - private string DebuggerDisplay => HasValue && Value != null - ? $"{Value.GetType().GetProperty("DebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(Value) ?? Value.ToString()} (Cacheable)" - : $"{Id} (Cacheable, {typeof(TEntity).Name})"; -#endif } /// @@ -84,9 +74,6 @@ public Cacheable(TEntity? value, TId id, bool hasValue, Func> dow /// 可从 API 请求下载的实体的类型。 /// 共同继承或实现的类型。 /// 可延迟加载的缓存实体的 ID 的类型。 -#if DEBUG -[DebuggerDisplay("{DebuggerDisplay,nq}")] -#endif public readonly struct Cacheable where TCachedEntity : IEntity, TRelationship where TDownloadableEntity : IEntity, TRelationship @@ -145,9 +132,4 @@ public Cacheable(TCachedEntity? value, TId id, bool hasValue, Func public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); -#if DEBUG - private string DebuggerDisplay => HasValue && Value != null - ? $"{Value.GetType().GetProperty("DebuggerDisplay", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(Value) ?? Value.ToString()} (Cacheable)" - : $"{Id} (Cacheable, {typeof(TRelationship).Name})"; -#endif } diff --git a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/IKookClientServiceConfigurator.cs b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/IKookClientServiceConfigurator.cs index 963b41b3..eb3f5131 100644 --- a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/IKookClientServiceConfigurator.cs +++ b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/IKookClientServiceConfigurator.cs @@ -1,4 +1,5 @@ -using Kook.Rest; +using System.Diagnostics.CodeAnalysis; +using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; using Microsoft.Extensions.Options; @@ -32,7 +33,8 @@ public interface IKookClientServiceConfigurator : IKookClientConfiguratorComplet /// 客户端的类型。 /// 配置的类型。 /// 配置了基于 Webhook 的网关客户端的配置器。 - IKookClientConfigurator UseWebhookClient( + IKookClientConfigurator UseWebhookClient( Func, TClient> clientFactory, Action configure) where TClient : KookWebhookClient where TConfig : KookWebhookConfig; diff --git a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookClientServiceConfigurator.cs b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookClientServiceConfigurator.cs index cef84f7e..37b965ac 100644 --- a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookClientServiceConfigurator.cs +++ b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookClientServiceConfigurator.cs @@ -1,4 +1,5 @@ -using Kook.Rest; +using System.Diagnostics.CodeAnalysis; +using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; using Microsoft.Extensions.DependencyInjection; @@ -36,7 +37,8 @@ public IKookClientConfigurator UseSocketClie } /// - public IKookClientConfigurator UseWebhookClient( + public IKookClientConfigurator UseWebhookClient( Func, TClient> clientFactory, Action configure) where TClient : KookWebhookClient where TConfig : KookWebhookConfig diff --git a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookWebhookClientConfigurator.cs b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookWebhookClientConfigurator.cs index 1a872391..de800030 100644 --- a/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookWebhookClientConfigurator.cs +++ b/src/Kook.Net.DependencyInjection.Microsoft/Configurators/KookWebhookClientConfigurator.cs @@ -1,10 +1,13 @@ -using Kook.Webhook; +using System.Diagnostics.CodeAnalysis; +using Kook.Webhook; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Kook.Net.DependencyInjection.Microsoft; -internal class KookWebhookClientConfigurator : KookClientConfigurator +internal class KookWebhookClientConfigurator : + KookClientConfigurator where TClient : KookWebhookClient where TConfig : KookWebhookConfig { diff --git a/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.Keyed.cs b/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.Keyed.cs index a0b088b1..7827664d 100644 --- a/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.Keyed.cs +++ b/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.Keyed.cs @@ -1,4 +1,5 @@ -using Kook.Rest; +using System.Diagnostics.CodeAnalysis; +using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; using Microsoft.Extensions.DependencyInjection; @@ -147,13 +148,15 @@ public static IServiceCollection AddKeyedKookWebhookClient(thi /// Webhook 客户端的类型。 /// Webhook 客户端的配置类型。 /// 添加了 KOOK Webhook 客户端的服务集合。 - public static IServiceCollection AddKeyedKookWebhookClient(this IServiceCollection services, + public static IServiceCollection AddKeyedKookWebhookClient(this IServiceCollection services, string? serviceKey, Func clientFactory) where TClient: KookWebhookClient where TConfig: KookWebhookConfig { - services.AddKeyedSingleton(serviceKey, (provider, _) => clientFactory(provider, provider.GetRequiredService>().Get(serviceKey))); + services.AddKeyedSingleton(serviceKey, (provider, _) => + clientFactory(provider, provider.GetRequiredService>().Get(serviceKey))); services.AddKeyedKookWebhookClient(serviceKey); return services; } diff --git a/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.cs b/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.cs index 22348163..807ae628 100644 --- a/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.cs +++ b/src/Kook.Net.DependencyInjection.Microsoft/KookClientDependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ -using Kook.Rest; +using System.Diagnostics.CodeAnalysis; +using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; using Microsoft.Extensions.DependencyInjection; @@ -120,10 +121,12 @@ public static IServiceCollection AddKookSocketClient(this IServiceCollection ser /// Webhook 客户端的类型。 /// Webhook 客户端的配置类型。 /// 添加了 KOOK Webhook 客户端的服务集合。 - public static IServiceCollection AddKookWebhookClient(this IServiceCollection services, + public static IServiceCollection AddKookWebhookClient( + this IServiceCollection services, Func, TClient> clientFactory, Action configure) - where TClient: KookWebhookClient - where TConfig: KookWebhookConfig + where TClient : KookWebhookClient + where TConfig : KookWebhookConfig { services.Configure(configure); services.AddKookWebhookClient(clientFactory); @@ -159,12 +162,15 @@ public static IServiceCollection AddKookWebhookClient(this ISe /// Webhook 客户端的类型。 /// Webhook 客户端的配置类型。 /// 添加了 KOOK Webhook 客户端的服务集合。 - public static IServiceCollection AddKookWebhookClient(this IServiceCollection services, + public static IServiceCollection AddKookWebhookClient( + this IServiceCollection services, Func, TClient> clientFactory) - where TClient: KookWebhookClient - where TConfig: KookWebhookConfig + where TClient : KookWebhookClient + where TConfig : KookWebhookConfig { - services.AddSingleton(provider => clientFactory(provider, provider.GetRequiredService>())); + services.AddSingleton(provider => + clientFactory(provider, provider.GetRequiredService>())); services.AddKookWebhookClient(); return services; } diff --git a/src/Kook.Net.Experimental/Net/Contexts/KookExperimentalJsonSerializerContexts.cs b/src/Kook.Net.Experimental/Net/Contexts/KookExperimentalJsonSerializerContexts.cs new file mode 100644 index 00000000..d9b1118f --- /dev/null +++ b/src/Kook.Net.Experimental/Net/Contexts/KookExperimentalJsonSerializerContexts.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using Kook.API.Rest; + +namespace Kook.Net.Contexts; + +/// +/// Provides JSON serialization context for Native AOT compatibility. +/// +[JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] +[JsonSerializable(typeof(IReadOnlyCollection))] +[JsonSerializable(typeof(ValidateCardsParams))] +internal partial class KookExperimentalJsonSerializerContexts : JsonSerializerContext; diff --git a/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs b/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs index 241b8beb..690d09bb 100644 --- a/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs +++ b/src/Kook.Net.Experimental/Rest/ExperimentalClientHelper.cs @@ -39,7 +39,7 @@ public static Task ValidateCardsAsync(KookRestClient client, string cardsJson, R #endregion - #region MyRegion + #region ThreadTag public static async Task> QueryThreadTagsAsync(KookRestClient client, string keyword, RequestOptions? options) diff --git a/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs b/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs index 81995710..ad7bb32e 100644 --- a/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs +++ b/src/Kook.Net.Experimental/Rest/KookRestApiClientExperimentalExtensions.cs @@ -4,17 +4,27 @@ using Kook.API; using Kook.API.Rest; +using Kook.Net.Contexts; using Kook.Net.Queue; namespace Kook.Rest.Extensions; internal static class KookRestApiClientExperimentalExtensions { + private static readonly ConcurrentHashSet _injectedInstances = []; + + private static void EnsureJsonTypeInfosInjection(this KookRestApiClient client) + { + if (_injectedInstances.TryAdd(client.GetHashCode())) + client.InjectJsonTypeInfos(KookExperimentalJsonSerializerContexts.Default.Options.TypeInfoResolverChain); + } + #region Guilds public static IAsyncEnumerable> GetAdminGuildsAsync(this KookRestApiClient client, int limit = KookConfig.MaxItemsPerBatchByDefault, int fromPage = 1, RequestOptions? options = null) { + client.EnsureJsonTypeInfosInjection(); options = RequestOptions.CreateOrClone(options); KookRestApiClient.BucketIds ids = new(); return client.SendPagedAsync(HttpMethod.Get, @@ -29,6 +39,7 @@ public static IAsyncEnumerable> GetAdminGuildsAsync(t public static async Task ValidateCardsAsync(this KookRestApiClient client, ValidateCardsParams args, RequestOptions? options = null) { + client.EnsureJsonTypeInfosInjection(); Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -46,6 +57,7 @@ await client.SendJsonAsync(HttpMethod.Post, public static async Task> QueryThreadTagsAsync(this KookRestApiClient client, string keyword, RequestOptions? options = null) { + client.EnsureJsonTypeInfosInjection(); Preconditions.NotNullOrEmpty(keyword, nameof(keyword)); options = RequestOptions.CreateOrClone(options); diff --git a/src/Kook.Net.Hosting/KookClientConfiguratorExtensions.cs b/src/Kook.Net.Hosting/KookClientConfiguratorExtensions.cs index 854fafde..7685b4ba 100644 --- a/src/Kook.Net.Hosting/KookClientConfiguratorExtensions.cs +++ b/src/Kook.Net.Hosting/KookClientConfiguratorExtensions.cs @@ -1,4 +1,5 @@ -using Kook.Net.DependencyInjection.Microsoft; +using System.Diagnostics.CodeAnalysis; +using Kook.Net.DependencyInjection.Microsoft; using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; @@ -140,7 +141,8 @@ public static IKookClientConfigurator UseHos /// 客户端的类型。 /// 配置的类型。 /// 配置了基于 Webhook 的网关客户端及客户端托管服务的 KOOK 客户端配置器。 - public static IKookClientConfigurator UseHostedWebhookClient( + public static IKookClientConfigurator UseHostedWebhookClient( this IKookClientServiceConfigurator configurator, Func, TClient> clientFactory, Action configure, TokenType tokenType, string token, bool validateToken = true) where TClient : KookWebhookClient @@ -161,7 +163,8 @@ public static IKookClientConfigurator UseHostedWebhookClient 客户端的类型。 /// 配置的类型。 /// 配置了基于 Webhook 的网关客户端及客户端托管服务的 KOOK 客户端配置器。 - public static IKookClientConfigurator UseHostedWebhookClient( + public static IKookClientConfigurator UseHostedWebhookClient( this IKookClientServiceConfigurator configurator, Func, TClient> clientFactory, Action configure, Func tokenType, Func token, Func? validateToken = null) diff --git a/src/Kook.Net.Hosting/KookClientHostExtensions.cs b/src/Kook.Net.Hosting/KookClientHostExtensions.cs index a23f966e..7475f599 100644 --- a/src/Kook.Net.Hosting/KookClientHostExtensions.cs +++ b/src/Kook.Net.Hosting/KookClientHostExtensions.cs @@ -1,4 +1,5 @@ -using Kook.Net.DependencyInjection.Microsoft; +using System.Diagnostics.CodeAnalysis; +using Kook.Net.DependencyInjection.Microsoft; using Kook.Rest; using Kook.Webhook; using Kook.WebSocket; @@ -195,7 +196,8 @@ public static IServiceCollection AddHostedKookSocketClient(this IServiceCollecti /// 客户端的类型。 /// 配置的类型。 /// 添加了基于 Webhook 的 KOOK 网关客户端及服务的服务集合。 - public static IServiceCollection AddHostedKookWebhookClient(this IServiceCollection services, + public static IServiceCollection AddHostedKookWebhookClient(this IServiceCollection services, Func, TClient> clientFactory, Action configure, Func tokenType, Func token, Func? validateToken = null) @@ -255,7 +257,8 @@ public static IServiceCollection AddHostedKookWebhookClient(th /// 客户端的类型。 /// 配置的类型。 /// 添加了基于 Webhook 的 KOOK 网关客户端及服务的服务集合。 - public static IServiceCollection AddHostedKookWebhookClient(this IServiceCollection services, + public static IServiceCollection AddHostedKookWebhookClient(this IServiceCollection services, Func, TClient> clientFactory, Action configure, TokenType tokenType, string token, bool validateToken = true) where TClient : KookWebhookClient diff --git a/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs index dd5d191c..5a93757c 100644 --- a/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Kook.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -1,7 +1,10 @@ using Kook.API; using Kook.API.Rest; using System.Collections.Immutable; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Model = Kook.API.Channel; namespace Kook.Rest; @@ -226,12 +229,19 @@ public static async Task> SendMessageAsync(IMes BaseKookClient client, MessageType messageType, ulong templateId, T parameters, IQuote? quote, IUser? ephemeralUser, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + CreateMessageParams args = new() { Type = messageType, ChannelId = channel.Id, TemplateId = templateId, - Content = JsonSerializer.Serialize(parameters, jsonSerializerOptions), + Content = JsonSerializer.Serialize(parameters, typeInfo), QuotedMessageId = MessageHelper.QuoteToReferenceMessageId(quote), ReplyMessageId = MessageHelper.QuoteToReplyMessageId(quote), EphemeralUserId = ephemeralUser?.Id @@ -252,7 +262,13 @@ public static async Task> SendCardsAsync(IMessa BaseKookClient client, ulong templateId, T parameters, IQuote? quote, IUser? ephemeralUser, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { - string json = JsonSerializer.Serialize(parameters, jsonSerializerOptions); + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + string json = JsonSerializer.Serialize(parameters, typeInfo); return await SendMessageAsync(channel, client, MessageType.Card, templateId, json, quote, ephemeralUser, jsonSerializerOptions, options); } @@ -437,12 +453,19 @@ public static async Task> SendDirectMessageAsync(I public static async Task> SendDirectMessageAsync(IDMChannel channel, BaseKookClient client, MessageType messageType, ulong templateId, T parameters, IQuote? quote, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + CreateDirectMessageParams args = new() { Type = messageType, UserId = channel.Recipient.Id, TemplateId = templateId, - Content = JsonSerializer.Serialize(parameters, jsonSerializerOptions), + Content = JsonSerializer.Serialize(parameters, typeInfo), QuotedMessageId = MessageHelper.QuoteToReferenceMessageId(quote), ReplyMessageId = MessageHelper.QuoteToReplyMessageId(quote) }; @@ -462,7 +485,13 @@ public static Task> SendDirectCardsAsync(IDMChanne public static Task> SendDirectCardsAsync(IDMChannel channel, BaseKookClient client, ulong templateId, T parameters, IQuote? quote, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { - string json = JsonSerializer.Serialize(parameters, jsonSerializerOptions); + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + string json = JsonSerializer.Serialize(parameters, typeInfo); return SendDirectMessageAsync(channel, client, MessageType.Card, templateId, json, quote, jsonSerializerOptions, options); } diff --git a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs index 01c0feaf..d330e02d 100644 --- a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs @@ -5,7 +5,9 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; +using Kook.Net.Contexts; using UserModel = Kook.API.User; namespace Kook.Rest; @@ -249,11 +251,18 @@ public static async Task ModifyAsync(Guid msgId, BaseKookClient client, string c public static async Task ModifyAsync(Guid msgId, BaseKookClient client, ulong templateId, T parameters, IQuote? quote, IUser? ephemeralUser, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + ModifyMessageParams args = new() { MessageId = msgId, TemplateId = templateId, - Content = JsonSerializer.Serialize(parameters, jsonSerializerOptions), + Content = JsonSerializer.Serialize(parameters, typeInfo), QuotedMessageId = QuoteToReferenceMessageId(quote), ReplyMessageId = QuoteToReplyMessageId(quote), EphemeralUserId = ephemeralUser?.Id @@ -350,11 +359,18 @@ public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, public static async Task ModifyDirectAsync(Guid msgId, BaseKookClient client, ulong templateId, T parameters, IQuote? quote, JsonSerializerOptions? jsonSerializerOptions, RequestOptions? options) { + JsonSerializerOptions serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(typeof(T)); + ModifyDirectMessageParams args = new() { TemplateId = templateId, MessageId = msgId, - Content = JsonSerializer.Serialize(parameters, jsonSerializerOptions), + Content = JsonSerializer.Serialize(parameters, typeInfo), QuotedMessageId = QuoteToReferenceMessageId(quote), ReplyMessageId = QuoteToReplyMessageId(quote) }; @@ -395,15 +411,17 @@ public static async Task UnpinAsync(Guid messageId, ulong channelId, BaseKookCli await client.ApiClient.UnpinAsync(args, options).ConfigureAwait(false); } + private static readonly JsonSerializerOptions serializerOptions = new(KookRestJsonSerializerContext.Default.Options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false, + Converters = { CardConverterFactory.Instance } + }; + public static ImmutableArray ParseCards(string json) { - JsonSerializerOptions serializerOptions = new() - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { CardConverterFactory.Instance } - }; - CardBase[]? cardBases = JsonSerializer.Deserialize(json, serializerOptions); + JsonTypeInfo typeInfo = serializerOptions.GetTypedTypeInfo(); + CardBase[]? cardBases = JsonSerializer.Deserialize(json, typeInfo); if (cardBases is null) throw new InvalidOperationException("Failed to parse cards from the provided JSON."); return [..cardBases.Select(x => x.ToEntity())]; @@ -416,14 +434,9 @@ public static string SerializeCards(IEnumerable cards) Preconditions.AtMost(enumerable.Sum(c => c.ModuleCount), maxModuleCount, nameof(cards), $"A max of {maxModuleCount} modules can be included in a card."); - JsonSerializerOptions serializerOptions = new() - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { CardConverterFactory.Instance }, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - return JsonSerializer.Serialize(enumerable.Select(c => c.ToModel()), serializerOptions); + CardBase[] cardBases = enumerable.Select(c => c.ToModel()).ToArray(); + JsonTypeInfo typeInfo = serializerOptions.GetTypedTypeInfo(); + return JsonSerializer.Serialize(cardBases, typeInfo); } public static IReadOnlyCollection ParseAttachments(IEnumerable cards) diff --git a/src/Kook.Net.Rest/Entities/Threads/ThreadHelper.cs b/src/Kook.Net.Rest/Entities/Threads/ThreadHelper.cs index 1988a43d..129c6ce4 100644 --- a/src/Kook.Net.Rest/Entities/Threads/ThreadHelper.cs +++ b/src/Kook.Net.Rest/Entities/Threads/ThreadHelper.cs @@ -1,6 +1,4 @@ using System.Collections.Immutable; -using System.Runtime.CompilerServices; -using System.Text.Json; using Kook.API; using Kook.API.Rest; using Thread = Kook.API.Thread; diff --git a/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs b/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs index 4aeab0ab..5aea8019 100644 --- a/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs +++ b/src/Kook.Net.Rest/Extensions/CardJsonExtension.cs @@ -1,8 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Kook.API; +using Kook.Net.Contexts; using Kook.Net.Converters; namespace Kook.Rest; @@ -12,12 +13,13 @@ namespace Kook.Rest; /// public static class CardJsonExtension { - private static readonly Lazy _options = new(() => new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { CardConverterFactory.Instance } - }); + private static readonly Lazy _options = new(() => + new JsonSerializerOptions(KookRestJsonSerializerContext.Default.Options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { CardConverterFactory.Instance } + } + ); /// /// 尝试将字符串解析为单个卡片构造器 。 @@ -29,7 +31,7 @@ public static bool TryParseSingle(string json, [NotNullWhen(true)] out ICardBuil { try { - CardBase? model = JsonSerializer.Deserialize(json, _options.Value); + CardBase? model = JsonSerializer.Deserialize(json, _options.Value.GetTypedTypeInfo()); if (model is not null) { @@ -57,7 +59,7 @@ public static bool TryParseMany(string json, [NotNullWhen(true)] out IEnumerable { try { - IEnumerable? models = JsonSerializer.Deserialize>(json, _options.Value); + IEnumerable? models = JsonSerializer.Deserialize(json, _options.Value.GetTypedTypeInfo>()); if (models is not null) { @@ -83,7 +85,7 @@ public static bool TryParseMany(string json, [NotNullWhen(true)] out IEnumerable /// 如果无法将 JSON 解析为单个卡片构造器。 public static ICardBuilder ParseSingle(string json) { - CardBase model = JsonSerializer.Deserialize(json, _options.Value) + CardBase model = JsonSerializer.Deserialize(json, _options.Value.GetTypedTypeInfo()) ?? throw new JsonException("Unable to parse json into card."); return model.ToEntity().ToBuilder(); } @@ -96,7 +98,8 @@ public static ICardBuilder ParseSingle(string json) /// 如果无法将 JSON 解析为多个卡片构造器。 public static IEnumerable ParseMany(string json) { - IEnumerable models = JsonSerializer.Deserialize>(json, _options.Value) + JsonTypeInfo> typeInfo = _options.Value.GetTypedTypeInfo>(); + IEnumerable models = JsonSerializer.Deserialize(json, typeInfo) ?? throw new JsonException("Unable to parse json into cards."); return models.Select(x => x.ToEntity().ToBuilder()); } @@ -118,14 +121,14 @@ public static string ToJsonString(this ICardBuilder builder, bool writeIndented /// 包含来自 的数据的 JSON 字符串。 public static string ToJsonString(this ICard card, bool writeIndented = true) { - JsonSerializerOptions options = new() + JsonSerializerOptions options = new(KookRestJsonSerializerContext.Default.Options) { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, WriteIndented = writeIndented, - Converters = { CardConverterFactory.Instance }, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + Converters = { CardConverterFactory.Instance } }; - return JsonSerializer.Serialize(card.ToModel(), options); + CardBase model = card.ToModel(); + JsonTypeInfo typeInfo = options.GetTypeInfo(model.GetType()); + return JsonSerializer.Serialize(model, typeInfo); } } diff --git a/src/Kook.Net.Rest/Extensions/JsonSerializerOptionsExtensions.cs b/src/Kook.Net.Rest/Extensions/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000..cb1fc6db --- /dev/null +++ b/src/Kook.Net.Rest/Extensions/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Kook.Rest; + +/// +/// Provides extension methods for . +/// +internal static class JsonSerializerOptionsExtensions +{ + /// + /// Gets the for the specified type from the . + /// + /// The type to get the type info for. + /// The serializer options containing the type resolver. + /// The for the specified type. + /// + /// Thrown when the type info cannot be resolved from the options. + /// + public static JsonTypeInfo GetTypedTypeInfo(this JsonSerializerOptions options) + { + if (options.TypeInfoResolver?.GetTypeInfo(typeof(T), options) is JsonTypeInfo typeInfo) + return typeInfo; + + throw new InvalidOperationException( + $"Unable to resolve JsonTypeInfo for type {typeof(T).FullName}. " + + "Ensure the type is registered in the JsonSerializerContext."); + } +} diff --git a/src/Kook.Net.Rest/KookRestApiClient.cs b/src/Kook.Net.Rest/KookRestApiClient.cs index 164cab6d..171282b7 100644 --- a/src/Kook.Net.Rest/KookRestApiClient.cs +++ b/src/Kook.Net.Rest/KookRestApiClient.cs @@ -12,11 +12,14 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Kook.API.Rest; using Kook.Net; +using Kook.Net.Contexts; using Kook.Net.Converters; using Kook.Net.Queue; using Kook.Net.Rest; +using Kook.Rest; namespace Kook.API; @@ -36,7 +39,7 @@ public event Func SentRequest private readonly AsyncEvent> _sentRequestEvent = new(); - protected readonly JsonSerializerOptions _serializerOptions; + protected JsonSerializerOptions _serializerOptions; protected readonly SemaphoreSlim _stateLock; private readonly RestClientProvider _restClientProvider; @@ -61,13 +64,8 @@ public KookRestApiClient(RestClientProvider restClientProvider, string userAgent _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; - _serializerOptions = serializerOptions - ?? new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { CardConverterFactory.Instance } - }; + _serializerOptions = EnsureJsonSerializerOptions(serializerOptions); + InjectJsonTypeInfos(KookRestJsonSerializerContext.Default.Options.TypeInfoResolverChain); DefaultRatelimitCallback = defaultRatelimitCallback; RequestQueue = new RequestQueue(); @@ -96,6 +94,27 @@ internal static string GetPrefixedToken(TokenType tokenType, string token) => _ => throw new ArgumentException("Unknown OAuth token type.", nameof(tokenType)) }; + protected static JsonSerializerOptions EnsureJsonSerializerOptions(JsonSerializerOptions? jsonSerializerOptions) + { + JsonSerializerOptions options = jsonSerializerOptions is not null + ? new JsonSerializerOptions(jsonSerializerOptions) + : new JsonSerializerOptions(); + options.WriteIndented = false; + options.NumberHandling = JsonNumberHandling.AllowReadingFromString; + options.Converters.Add(CardConverterFactory.Instance); + return options; + } + + protected internal void InjectJsonTypeInfos(IList typeInfoResolvers) + { + JsonSerializerOptions serializerOptions = _serializerOptions.IsReadOnly + ? new JsonSerializerOptions(_serializerOptions) + : _serializerOptions; + foreach (IJsonTypeInfoResolver typeInfoResolver in typeInfoResolvers) + serializerOptions.TypeInfoResolverChain.Add(typeInfoResolver); + Interlocked.Exchange(ref _serializerOptions, serializerOptions); + } + internal virtual void Dispose(bool disposing) { if (!_isDisposed) @@ -273,7 +292,7 @@ public async Task SendAsync(HttpMethod method, string endp Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); return bypassDeserialization && response is TResponse responseObj ? responseObj - : await DeserializeJsonAsync(response).ConfigureAwait(false); + : await DeserializeJsonAsync(response, _serializerOptions).ConfigureAwait(false); } internal async Task SendJsonAsync(HttpMethod method, @@ -298,7 +317,7 @@ public async Task SendJsonAsync(HttpMethod method, string Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); return bypassDeserialization && response is TResponse responseObj ? responseObj - : await DeserializeJsonAsync(response).ConfigureAwait(false); + : await DeserializeJsonAsync(response, _serializerOptions).ConfigureAwait(false); } internal Task SendMultipartAsync(HttpMethod method, @@ -323,7 +342,7 @@ public async Task SendMultipartAsync(HttpMethod method, Stream response = await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); return bypassDeserialization && response is TResponse responseObj ? responseObj - : await DeserializeJsonAsync(response).ConfigureAwait(false); + : await DeserializeJsonAsync(response, _serializerOptions).ConfigureAwait(false); } private async Task SendInternalAsync(HttpMethod method, string endpoint, RestRequest request) @@ -413,6 +432,10 @@ public async Task GetGuildAsync(ulong guildId, RequestOptions? op return response?.UserCount; } +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL3050", + Justification = "Expression tree usage in bucket ID generation has fallback logic and works in AOT scenarios.")] +#endif public IAsyncEnumerable> GetGuildMembersAsync(ulong guildId, Action? func = null, int limit = KookConfig.MaxUsersPerBatch, int fromPage = 1, RequestOptions? options = null) @@ -1976,14 +1999,23 @@ protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000.0, 2); [return: NotNullIfNotNull(nameof(payload))] - protected string? SerializeJson(object? payload, JsonSerializerOptions? options = null) => - payload is null ? null : JsonSerializer.Serialize(payload, options ?? _serializerOptions); + protected string? SerializeJson(object? payload, JsonSerializerOptions? options = null) + { + if (payload is null) + return null; + + JsonSerializerOptions serializerOptions = options ?? _serializerOptions; + JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(payload.GetType()); + return JsonSerializer.Serialize(payload, typeInfo); + } - protected async Task DeserializeJsonAsync(Stream jsonStream) + protected static async Task DeserializeJsonAsync(Stream jsonStream, JsonSerializerOptions jsonSerializerOptions) + where T : class { try { - T? jsonObject = await JsonSerializer.DeserializeAsync(jsonStream, _serializerOptions).ConfigureAwait(false); + JsonTypeInfo typeInfo = jsonSerializerOptions.GetTypedTypeInfo(); + T? jsonObject = await JsonSerializer.DeserializeAsync(jsonStream, typeInfo).ConfigureAwait(false); if (jsonObject is null) throw new JsonException($"Failed to deserialize JSON to type {typeof(T).FullName}"); return jsonObject; @@ -2048,7 +2080,16 @@ private static BucketId GetBucketId(HttpMethod httpMethod, BucketIds ids, Expres { Preconditions.NotNull(callingMethod, nameof(callingMethod)); ids.HttpMethod = httpMethod; - return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); + Func func = _bucketIdGenerators.GetOrAdd( + callingMethod, +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + static (_, arg) => CreateBucketId(arg), + endpointExpr +#else + _ => CreateBucketId(endpointExpr) +#endif + ); + return func(ids); } private static BucketId GetBucketId(HttpMethod httpMethod, BucketIds ids, Expression> endpointExpr, @@ -2056,12 +2097,25 @@ private static BucketId GetBucketId(HttpMethod httpMethod, BucketI { Preconditions.NotNull(callingMethod, nameof(callingMethod)); ids.HttpMethod = httpMethod; - return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr, arg1, arg2))(ids); + Func func = _bucketIdGenerators.GetOrAdd( + callingMethod, +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + static (_, arg) => CreateBucketId(arg.endpointExpr, arg.arg1, arg.arg2), + (endpointExpr, arg1, arg2) +#else + _ => CreateBucketId(endpointExpr, arg1, arg2) +#endif + ); + return func(ids); } private static Func CreateBucketId(Expression> endpoint, TArg1 arg1, TArg2 arg2) => CreateBucketId(() => endpoint.Compile().Invoke(arg1, arg2)); +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL3050", + Justification = "Expression tree compilation is only used for bucket ID generation and has fallback logic.")] +#endif private static Func CreateBucketId(Expression> endpoint) { try diff --git a/src/Kook.Net.Rest/Net/Contexts/KookRestJsonSerializerContext.cs b/src/Kook.Net.Rest/Net/Contexts/KookRestJsonSerializerContext.cs new file mode 100644 index 00000000..00c762f0 --- /dev/null +++ b/src/Kook.Net.Rest/Net/Contexts/KookRestJsonSerializerContext.cs @@ -0,0 +1,198 @@ +using System.Text.Json.Serialization; +// ReSharper disable RedundantNameQualifier + +namespace Kook.Net.Contexts; + +/// +/// Provides JSON serialization context for Native AOT compatibility. +/// +[JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] +// REST Common Models +[JsonSerializable(typeof(Kook.API.Rest.RestResponseBase))] +// Common Models +[JsonSerializable(typeof(Kook.API.Attachment))] +[JsonSerializable(typeof(Kook.API.Ban))] +[JsonSerializable(typeof(Kook.API.Channel))] +[JsonSerializable(typeof(Kook.API.DirectMessage))] +[JsonSerializable(typeof(Kook.API.Emoji))] +[JsonSerializable(typeof(Kook.API.ExtendedThread))] +[JsonSerializable(typeof(Kook.API.ExtendedThreadPost))] +[JsonSerializable(typeof(Kook.API.Game))] +[JsonSerializable(typeof(Kook.API.Guild))] +[JsonSerializable(typeof(Kook.API.GuildCertification))] +[JsonSerializable(typeof(Kook.API.GuildMentionInfo))] +[JsonSerializable(typeof(Kook.API.Intimacy))] +[JsonSerializable(typeof(Kook.API.Invite))] +[JsonSerializable(typeof(Kook.API.MentionedChannel))] +[JsonSerializable(typeof(Kook.API.MentionedUser))] +[JsonSerializable(typeof(Kook.API.MentionInfo))] +[JsonSerializable(typeof(Kook.API.Message))] +[JsonSerializable(typeof(Kook.API.MessageTemplate))] +[JsonSerializable(typeof(Kook.API.Nameplate))] +[JsonSerializable(typeof(Kook.API.Quote))] +[JsonSerializable(typeof(Kook.API.Reaction))] +[JsonSerializable(typeof(Kook.API.RecommendInfo))] +[JsonSerializable(typeof(Kook.API.Role))] +[JsonSerializable(typeof(Kook.API.RolePermissionOverwrite))] +[JsonSerializable(typeof(Kook.API.Thread))] +[JsonSerializable(typeof(Kook.API.ThreadAttachmentType))] +[JsonSerializable(typeof(Kook.API.ThreadMedia))] +[JsonSerializable(typeof(Kook.API.ThreadPost))] +[JsonSerializable(typeof(Kook.API.ThreadTag))] +[JsonSerializable(typeof(Kook.API.User))] +[JsonSerializable(typeof(Kook.API.UserChat))] +[JsonSerializable(typeof(Kook.API.UserPermissionOverwrite))] +[JsonSerializable(typeof(Kook.API.UserTag))] +// Card Models +[JsonSerializable(typeof(Kook.API.Card))] +[JsonSerializable(typeof(Kook.API.CardBase))] +[JsonSerializable(typeof(Kook.API.ICard))] +[JsonSerializable(typeof(Kook.API.ActionGroupModule))] +[JsonSerializable(typeof(Kook.API.AudioModule))] +[JsonSerializable(typeof(Kook.API.ContainerModule))] +[JsonSerializable(typeof(Kook.API.ContextModule))] +[JsonSerializable(typeof(Kook.API.CountdownModule))] +[JsonSerializable(typeof(Kook.API.DividerModule))] +[JsonSerializable(typeof(Kook.API.FileModule))] +[JsonSerializable(typeof(Kook.API.HeaderModule))] +[JsonSerializable(typeof(Kook.API.ImageGroupModule))] +[JsonSerializable(typeof(Kook.API.IModule))] +[JsonSerializable(typeof(Kook.API.InviteModule))] +[JsonSerializable(typeof(Kook.API.ModuleBase))] +[JsonSerializable(typeof(Kook.API.SectionModule))] +[JsonSerializable(typeof(Kook.API.VideoModule))] +[JsonSerializable(typeof(Kook.API.ButtonElement))] +[JsonSerializable(typeof(Kook.API.ElementBase))] +[JsonSerializable(typeof(Kook.API.IElement))] +[JsonSerializable(typeof(Kook.API.ImageElement))] +[JsonSerializable(typeof(Kook.API.KMarkdownElement))] +[JsonSerializable(typeof(Kook.API.ParagraphStruct))] +[JsonSerializable(typeof(Kook.API.PlainTextElement))] +// Embed Models +[JsonSerializable(typeof(Kook.API.BilibiliVideoEmbed))] +[JsonSerializable(typeof(Kook.API.CardEmbed))] +[JsonSerializable(typeof(Kook.API.EmbedBase))] +[JsonSerializable(typeof(Kook.API.IEmbed))] +[JsonSerializable(typeof(Kook.API.ImageEmbed))] +[JsonSerializable(typeof(Kook.API.LinkEmbed))] +[JsonSerializable(typeof(Kook.API.NotImplementedEmbed))] +// Poke Models +[JsonSerializable(typeof(Kook.API.ImageAnimationPokeResource))] +[JsonSerializable(typeof(Kook.API.IPokeResource))] +[JsonSerializable(typeof(Kook.API.NotImplementedPokeResource))] +[JsonSerializable(typeof(Kook.API.Poke))] +[JsonSerializable(typeof(Kook.API.PokeQualityResource))] +[JsonSerializable(typeof(Kook.API.PokeResourceBase))] +// REST Models +[JsonSerializable(typeof(Kook.API.Rest.AddOrRemoveRoleParams))] +[JsonSerializable(typeof(Kook.API.Rest.AddOrRemoveRoleResponse))] +[JsonSerializable(typeof(Kook.API.Rest.AddReactionParams))] +[JsonSerializable(typeof(Kook.API.Rest.BeginActivityParams))] +[JsonSerializable(typeof(Kook.API.Rest.BlockUserParams))] +[JsonSerializable(typeof(Kook.API.Rest.BoostSubscription))] +[JsonSerializable(typeof(Kook.API.Rest.ColorMap))] +[JsonSerializable(typeof(Kook.API.Rest.CreateAssetParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateAssetResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateDirectMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateDirectMessageResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGameParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildBanParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildEmoteParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildInviteParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildInviteResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateGuildRoleParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateMessageResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateMessageTemplateParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateOrModifyChannelPermissionOverwriteResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateOrModifyMessageTemplateResponse))] +[JsonSerializable(typeof(Kook.API.Rest.CreateOrRemoveChannelPermissionOverwriteParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateOrRemoveGuildMuteDeafParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreatePipeMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateThreadParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateThreadReplyParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateUserChatParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateVoiceGatewayParams))] +[JsonSerializable(typeof(Kook.API.Rest.CreateVoiceGatewayResponse))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteDirectMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteGameParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteGuildChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteGuildEmoteParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteGuildInviteParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteGuildRoleParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteMessageTemplateParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteThreadPostReplyParams))] +[JsonSerializable(typeof(Kook.API.Rest.DeleteUserChatParams))] +[JsonSerializable(typeof(Kook.API.Rest.DisposeVoiceGatewayParams))] +[JsonSerializable(typeof(Kook.API.Rest.EndGameActivityParams))] +[JsonSerializable(typeof(Kook.API.Rest.ExtendedGuild))] +[JsonSerializable(typeof(Kook.API.Rest.FriendState), TypeInfoPropertyName = "ApiFriendState")] +[JsonSerializable(typeof(Kook.API.Rest.GetBotGatewayResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GetChannelPermissionOverwritesResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GetFriendStatesResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GetGuildMemberCountResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GetGuildMuteDeafListResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GetThreadCategoriesResponse))] +[JsonSerializable(typeof(Kook.API.Rest.GuildMember))] +[JsonSerializable(typeof(Kook.API.Rest.HandleFriendRequestParams))] +[JsonSerializable(typeof(Kook.API.Rest.KeepVoiceGatewayAliveParams))] +[JsonSerializable(typeof(Kook.API.Rest.KickOutGuildMemberParams))] +[JsonSerializable(typeof(Kook.API.Rest.KickOutVoiceChannelUserParams))] +[JsonSerializable(typeof(Kook.API.Rest.LeaveGuildParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyChannelPermissionOverwriteParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyDirectMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyGameParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyGuildChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyGuildEmoteParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyGuildMemberNicknameParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyGuildRoleParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyMessageTemplateParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyTextChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyThreadChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.ModifyVoiceChannelParams))] +[JsonSerializable(typeof(Kook.API.Rest.MoveUsersParams))] +[JsonSerializable(typeof(Kook.API.Rest.MuteOrDeafType))] +[JsonSerializable(typeof(Kook.API.Rest.PinUnpinMessageParams))] +[JsonSerializable(typeof(Kook.API.Rest.QueryMessagesResponse))] +[JsonSerializable(typeof(Kook.API.Rest.QueryThreadsResponse))] +[JsonSerializable(typeof(Kook.API.Rest.QueryUserChatMessagesResponse))] +[JsonSerializable(typeof(Kook.API.Rest.ReactionUserResponse))] +[JsonSerializable(typeof(Kook.API.Rest.RemoveFriendParams))] +[JsonSerializable(typeof(Kook.API.Rest.RemoveGuildBanParams))] +[JsonSerializable(typeof(Kook.API.Rest.RemoveReactionParams))] +[JsonSerializable(typeof(Kook.API.Rest.RequestFriendParams))] +[JsonSerializable(typeof(Kook.API.Rest.RequestIntimacyParams))] +[JsonSerializable(typeof(Kook.API.Rest.RestResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.RichGuild))] +[JsonSerializable(typeof(Kook.API.Rest.SelfOnlineStatusResponse))] +[JsonSerializable(typeof(Kook.API.Rest.SelfUser))] +[JsonSerializable(typeof(Kook.API.Rest.SortMode))] +[JsonSerializable(typeof(Kook.API.Rest.SyncChannelPermissionsParams))] +[JsonSerializable(typeof(Kook.API.Rest.ThreadCategory))] +[JsonSerializable(typeof(Kook.API.Rest.ThreadCategoryPermissionOverwrite))] +[JsonSerializable(typeof(Kook.API.Rest.UnblockUserParams))] +[JsonSerializable(typeof(Kook.API.Rest.UnravelRelationParams))] +[JsonSerializable(typeof(Kook.API.Rest.UpdateIntimacyValueParams))] +[JsonSerializable(typeof(Kook.API.Rest.UserConfig))] +// Pages Models +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +[JsonSerializable(typeof(Kook.API.Rest.PagedResponseBase))] +// Misc Models +[JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(Kook.API.CardBase[]))] +internal partial class KookRestJsonSerializerContext : JsonSerializerContext; diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs index 20e01e7e..5931bfc9 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/CardConverter.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -15,7 +16,7 @@ internal class CardConverter : JsonConverter switch (jsonNode?["type"]?.GetValue()) { case "card": - return JsonSerializer.Deserialize(jsonNode.ToJsonString(), options); + return JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()); default: throw new ArgumentOutOfRangeException(nameof(CardType)); } @@ -23,10 +24,10 @@ internal class CardConverter : JsonConverter public override void Write(Utf8JsonWriter writer, CardBase value, JsonSerializerOptions options) { - switch (value.Type) + switch (value) { - case CardType.Card: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.Card, options)); + case API.Card { Type: CardType.Card } card: + writer.WriteRawValue(JsonSerializer.Serialize(card, options.GetTypedTypeInfo())); break; default: throw new ArgumentOutOfRangeException(nameof(CardType)); diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs index 4e9851c2..1dd77ca3 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ElementConverter.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -14,33 +15,33 @@ internal class ElementConverter : JsonConverter JsonNode? jsonNode = JsonNode.Parse(ref reader); return jsonNode?["type"]?.GetValue() switch { - "plain-text" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "kmarkdown" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "image" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "button" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "paragraph" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), + "plain-text" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "kmarkdown" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "image" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "button" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "paragraph" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), _ => throw new ArgumentOutOfRangeException(nameof(ElementType)) }; } public override void Write(Utf8JsonWriter writer, ElementBase value, JsonSerializerOptions options) { - switch (value.Type) + switch (value) { - case ElementType.PlainText: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.PlainTextElement, options)); + case API.PlainTextElement { Type: ElementType.PlainText } plaintext: + writer.WriteRawValue(JsonSerializer.Serialize(plaintext, options.GetTypedTypeInfo())); break; - case ElementType.KMarkdown: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.KMarkdownElement, options)); + case API.KMarkdownElement { Type: ElementType.KMarkdown } kMarkdown: + writer.WriteRawValue(JsonSerializer.Serialize(kMarkdown, options.GetTypedTypeInfo())); break; - case ElementType.Image: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ImageElement, options)); + case API.ImageElement { Type: ElementType.Image } image: + writer.WriteRawValue(JsonSerializer.Serialize(image, options.GetTypedTypeInfo())); break; - case ElementType.Button: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ButtonElement, options)); + case API.ButtonElement { Type: ElementType.Button } button: + writer.WriteRawValue(JsonSerializer.Serialize(button, options.GetTypedTypeInfo())); break; - case ElementType.Paragraph: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ParagraphStruct, options)); + case API.ParagraphStruct { Type: ElementType.Paragraph } paragraph: + writer.WriteRawValue(JsonSerializer.Serialize(paragraph, options.GetTypedTypeInfo())); break; default: throw new ArgumentOutOfRangeException(nameof(ElementType)); diff --git a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs index 8355edaa..6ffd7c81 100644 --- a/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Cards/ModuleConverter.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -14,61 +15,61 @@ internal class ModuleConverter : JsonConverter JsonNode? jsonNode = JsonNode.Parse(ref reader); return jsonNode?["type"]?.GetValue() switch { - "header" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "section" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "image-group" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "container" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "action-group" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "context" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "divider" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "file" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "audio" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "video" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "countdown" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "invite" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), + "header" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "section" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "image-group" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "container" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "action-group" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "context" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "divider" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "file" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "audio" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "video" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "countdown" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "invite" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), _ => throw new ArgumentOutOfRangeException(nameof(CardType)) }; } public override void Write(Utf8JsonWriter writer, ModuleBase value, JsonSerializerOptions options) { - switch (value.Type) + switch (value) { - case ModuleType.Header: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.HeaderModule, options)); + case API.HeaderModule { Type: ModuleType.Header } header: + writer.WriteRawValue(JsonSerializer.Serialize(header, options.GetTypedTypeInfo())); break; - case ModuleType.Section: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.SectionModule, options)); + case API.SectionModule { Type: ModuleType.Section } section: + writer.WriteRawValue(JsonSerializer.Serialize(section, options.GetTypedTypeInfo())); break; - case ModuleType.ImageGroup: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ImageGroupModule, options)); + case API.ImageGroupModule { Type: ModuleType.ImageGroup } imageGroup: + writer.WriteRawValue(JsonSerializer.Serialize(imageGroup, options.GetTypedTypeInfo())); break; - case ModuleType.Container: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ContainerModule, options)); + case API.ContainerModule { Type: ModuleType.Container } container: + writer.WriteRawValue(JsonSerializer.Serialize(container, options.GetTypedTypeInfo())); break; - case ModuleType.ActionGroup: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ActionGroupModule, options)); + case API.ActionGroupModule { Type: ModuleType.ActionGroup } actionGroup: + writer.WriteRawValue(JsonSerializer.Serialize(actionGroup, options.GetTypedTypeInfo())); break; - case ModuleType.Context: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ContextModule, options)); + case API.ContextModule { Type: ModuleType.Context } context: + writer.WriteRawValue(JsonSerializer.Serialize(context, options.GetTypedTypeInfo())); break; - case ModuleType.Divider: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.DividerModule, options)); + case API.DividerModule { Type: ModuleType.Divider } divider: + writer.WriteRawValue(JsonSerializer.Serialize(divider, options.GetTypedTypeInfo())); break; - case ModuleType.File: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.FileModule, options)); + case API.FileModule { Type: ModuleType.File } file: + writer.WriteRawValue(JsonSerializer.Serialize(file, options.GetTypedTypeInfo())); break; - case ModuleType.Audio: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.AudioModule, options)); + case API.AudioModule { Type: ModuleType.Audio } audio: + writer.WriteRawValue(JsonSerializer.Serialize(audio, options.GetTypedTypeInfo())); break; - case ModuleType.Video: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.VideoModule, options)); + case API.VideoModule { Type: ModuleType.Video } video: + writer.WriteRawValue(JsonSerializer.Serialize(video, options.GetTypedTypeInfo())); break; - case ModuleType.Countdown: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.CountdownModule, options)); + case API.CountdownModule { Type: ModuleType.Countdown } countdown: + writer.WriteRawValue(JsonSerializer.Serialize(countdown, options.GetTypedTypeInfo())); break; - case ModuleType.Invite: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.InviteModule, options)); + case API.InviteModule { Type: ModuleType.Invite } invite: + writer.WriteRawValue(JsonSerializer.Serialize(invite, options.GetTypedTypeInfo())); break; default: throw new ArgumentOutOfRangeException(nameof(ModuleType)); diff --git a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs index 9e119e11..8ec07438 100644 --- a/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Embeds/EmbedConverter.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -15,29 +16,29 @@ internal class EmbedConverter : JsonConverter if (rawType == null) return null; return rawType switch { - "link" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "image" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "bili-video" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), - "card" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), + "link" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "image" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "bili-video" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), + "card" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), _ => new API.NotImplementedEmbed(rawType, jsonNode) }; } public override void Write(Utf8JsonWriter writer, EmbedBase value, JsonSerializerOptions options) { - switch (value.Type) + switch (value) { - case EmbedType.Link: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.LinkEmbed, options)); + case API.LinkEmbed { Type: EmbedType.Link } link: + writer.WriteRawValue(JsonSerializer.Serialize(link, options.GetTypedTypeInfo())); break; - case EmbedType.Image: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ImageEmbed, options)); + case API.ImageEmbed { Type: EmbedType.Image } image: + writer.WriteRawValue(JsonSerializer.Serialize(image, options.GetTypedTypeInfo())); break; - case EmbedType.BilibiliVideo: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.BilibiliVideoEmbed, options)); + case API.BilibiliVideoEmbed { Type: EmbedType.BilibiliVideo } bilibiliVideo: + writer.WriteRawValue(JsonSerializer.Serialize(bilibiliVideo, options.GetTypedTypeInfo())); break; - case EmbedType.Card: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.CardEmbed, options)); + case API.CardEmbed { Type: EmbedType.Card } card: + writer.WriteRawValue(JsonSerializer.Serialize(card, options.GetTypedTypeInfo())); break; default: writer.WriteRawValue((value as API.NotImplementedEmbed)!.RawJsonNode.ToString()); diff --git a/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs b/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs index dac62e06..0086bbd7 100644 --- a/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/GuildFeaturesConverter.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -7,7 +8,7 @@ internal class GuildFeaturesConverter : JsonConverter { public override GuildFeatures Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - IList rawValues = JsonSerializer.Deserialize>(ref reader, options) ?? []; + IList rawValues = JsonSerializer.Deserialize(ref reader, options.GetTypedTypeInfo>()) ?? []; GuildFeature features = rawValues.Aggregate(GuildFeature.None, (current, item) => current | ApiStringToFeature(item)); return new GuildFeatures(features, rawValues); @@ -15,12 +16,15 @@ public override GuildFeatures Read(ref Utf8JsonReader reader, Type typeToConvert public override void Write(Utf8JsonWriter writer, GuildFeatures value, JsonSerializerOptions options) { - Array enumValues = Enum.GetValues(typeof(GuildFeature)); +#if NET8_0_OR_GREATER + IEnumerable values = Enum.GetValues(); +#else + IEnumerable values = Enum.GetValues(typeof(GuildFeature)).Cast(); +#endif writer.WriteStartArray(); - foreach (object enumValue in enumValues) + foreach (GuildFeature val in values) { - GuildFeature val = (GuildFeature)enumValue; if (val is GuildFeature.None) continue; diff --git a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs index e8d38c4c..d89be4f5 100644 --- a/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/Pokes/PokeResourceConverter.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -15,17 +16,17 @@ internal class PokeResourceConverter : JsonConverter if (rawType == null) return null; return rawType switch { - "ImageAnimation" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options), + "ImageAnimation" => JsonSerializer.Deserialize(jsonNode.ToJsonString(), options.GetTypedTypeInfo()), _ => new API.NotImplementedPokeResource(rawType, jsonNode) { Type = PokeResourceType.NotImplemented } }; } public override void Write(Utf8JsonWriter writer, PokeResourceBase value, JsonSerializerOptions options) { - switch (value.Type) + switch (value) { - case PokeResourceType.ImageAnimation: - writer.WriteRawValue(JsonSerializer.Serialize(value as API.ImageAnimationPokeResource, options)); + case API.ImageAnimationPokeResource { Type : PokeResourceType.ImageAnimation } imageAnimation: + writer.WriteRawValue(JsonSerializer.Serialize(imageAnimation, options.GetTypedTypeInfo())); break; default: writer.WriteRawValue((value as API.NotImplementedPokeResource)!.RawJsonNode.ToString()); diff --git a/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs b/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs index baeb3dc4..674024b4 100644 --- a/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/QuoteConverter.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Kook.Rest; namespace Kook.Net.Converters; @@ -14,7 +15,7 @@ internal class QuoteConverter : JsonConverter { JsonTokenType.Null or JsonTokenType.String => null, // 此转换器不会直接标记在 API.Quote 上,而是标记在属性上,因此,直接对 reader 反序列化为 API.Quote 的操作不会使用此转换器。 - JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, options), + JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, options.GetTypedTypeInfo()), _ => throw new JsonException( $"{nameof(QuoteConverter)} expects boolean, string or number token, but got {reader.TokenType}") }; diff --git a/src/Kook.Net.Rest/Net/Converters/TypeIdPermissionOverwriteTargetTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/TypeIdPermissionOverwriteTargetTypeConverter.cs index 4ada33f1..9ecf88e0 100644 --- a/src/Kook.Net.Rest/Net/Converters/TypeIdPermissionOverwriteTargetTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/TypeIdPermissionOverwriteTargetTypeConverter.cs @@ -1,4 +1,3 @@ -using Kook.API.Rest; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Kook.Net.Rest/Net/Converters/TypePermissionOverwriteTargetTypeConverter.cs b/src/Kook.Net.Rest/Net/Converters/TypePermissionOverwriteTargetTypeConverter.cs index 7dc25ab9..c8e24885 100644 --- a/src/Kook.Net.Rest/Net/Converters/TypePermissionOverwriteTargetTypeConverter.cs +++ b/src/Kook.Net.Rest/Net/Converters/TypePermissionOverwriteTargetTypeConverter.cs @@ -1,6 +1,5 @@ using System.Text.Json; using System.Text.Json.Serialization; -using Kook.API.Rest; namespace Kook.Net.Converters; diff --git a/src/Kook.Net.Rest/Net/DefaultRestClient.cs b/src/Kook.Net.Rest/Net/DefaultRestClient.cs index 661c2842..48f8f2e1 100644 --- a/src/Kook.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Kook.Net.Rest/Net/DefaultRestClient.cs @@ -13,7 +13,8 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; +using Kook.Net.Contexts; +using Kook.Net.Converters; namespace Kook.Net.Rest; @@ -44,10 +45,9 @@ public DefaultRestClient(string baseUrl, bool useProxy = false, IWebProxy? webPr _cancellationToken = CancellationToken.None; - _serializerOptions = new JsonSerializerOptions + _serializerOptions = new JsonSerializerOptions(KookRestJsonSerializerContext.Default.Options) { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; } diff --git a/src/Kook.Net.Rest/Net/Queue/RequestBucket.cs b/src/Kook.Net.Rest/Net/Queue/RequestBucket.cs index ccea65af..9190830b 100644 --- a/src/Kook.Net.Rest/Net/Queue/RequestBucket.cs +++ b/src/Kook.Net.Rest/Net/Queue/RequestBucket.cs @@ -2,8 +2,10 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; +using Kook.Net.Contexts; +using Kook.Net.Converters; using Kook.Net.Rest; +using Kook.Rest; namespace Kook.Net.Queue; @@ -24,10 +26,9 @@ internal class RequestBucket public RequestBucket(RequestQueue queue, IRequest request, BucketId id) { - _serializerOptions = new JsonSerializerOptions + _serializerOptions = new JsonSerializerOptions(KookRestJsonSerializerContext.Default.Options) { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; _queue = queue; Id = id; @@ -97,8 +98,8 @@ public async Task SendAsync(RestRequest request) if (response.Stream != null) try { - responseBase = await JsonSerializer.DeserializeAsync(response.Stream, - _serializerOptions); + responseBase = await JsonSerializer.DeserializeAsync( + response.Stream, _serializerOptions.GetTypedTypeInfo()); } catch { @@ -120,8 +121,8 @@ public async Task SendAsync(RestRequest request) KookDebugger.DebugRatelimit($"[Ratelimit] [{id}] Success"); if (response.MediaTypeHeader?.MediaType == "application/json") { - API.Rest.RestResponseBase? responseBase = - await JsonSerializer.DeserializeAsync(response.Stream, _serializerOptions); + API.Rest.RestResponseBase? responseBase = await JsonSerializer.DeserializeAsync( + response.Stream, _serializerOptions.GetTypedTypeInfo()); if (responseBase?.Code > (KookErrorCode)0) { throw new HttpException( diff --git a/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs b/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs index af2c77aa..1ce1722f 100644 --- a/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Kook.Net.Rest/Net/Queue/RequestQueue.cs @@ -164,7 +164,11 @@ internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? in public void ClearGatewayBuckets() { - foreach (GatewayBucketType gwBucket in (GatewayBucketType[])Enum.GetValues(typeof(GatewayBucketType))) +#if NET6_0_OR_GREATER + foreach (GatewayBucketType gwBucket in Enum.GetValues()) +#else + foreach (GatewayBucketType gwBucket in Enum.GetValues(typeof(GatewayBucketType)).Cast()) +#endif _buckets.TryRemove(GatewayBucket.Get(gwBucket).Id, out _); } diff --git a/src/Kook.Net.WebSocket/KookSocketApiClient.cs b/src/Kook.Net.WebSocket/KookSocketApiClient.cs index 547d9e01..d3583edc 100644 --- a/src/Kook.Net.WebSocket/KookSocketApiClient.cs +++ b/src/Kook.Net.WebSocket/KookSocketApiClient.cs @@ -1,13 +1,15 @@ using System.IO.Compression; using System.Text; -using System.Diagnostics; using System.Text.Encodings.Web; using System.Text.Json; using Kook.API.Gateway; using Kook.API.Rest; +using Kook.Net.Contexts; +using Kook.Net.Converters; using Kook.Net.Queue; using Kook.Net.Rest; using Kook.Net.WebSockets; +using Kook.Rest; using Kook.WebSocket; namespace Kook.API; @@ -44,12 +46,6 @@ public event Func Disconnected private Guid? _sessionId; private int _lastSeq; - private readonly JsonSerializerOptions _debugJsonSerializerOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public ConnectionState ConnectionState { get; private set; } internal IWebSocketClient WebSocketClient { get; } @@ -59,9 +55,10 @@ public KookSocketApiClient(RestClientProvider restClientProvider, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializerOptions? serializerOptions = null, Func? defaultRatelimitCallback = null) - : base(restClientProvider, userAgent, acceptLanguage, - defaultRetryMode, serializerOptions, defaultRatelimitCallback) + : base(restClientProvider, userAgent, acceptLanguage, defaultRetryMode, + serializerOptions, defaultRatelimitCallback) { + InjectJsonTypeInfos(KookWebSocketJsonSerializerContext.Default.Options.TypeInfoResolverChain); _gatewayUrl = url; if (url != null) _isExplicitUrl = true; @@ -97,19 +94,15 @@ private async Task OnBinaryMessage(byte[] data, int index, int count) decompressed.Position = 0; GatewaySocketFrame? gatewaySocketFrame = await JsonSerializer - .DeserializeAsync(decompressed, _serializerOptions); + .DeserializeAsync(decompressed, _serializerOptions.GetTypedTypeInfo()); if (gatewaySocketFrame is not null) { if (KookDebugger.IsDebuggingPacket) { string raw = Encoding.Default.GetString(decompressed.ToArray()).TrimEnd('\n'); - string parsed = JsonSerializer - .Serialize(gatewaySocketFrame.Payload, _debugJsonSerializerOptions) - .TrimEnd('\n'); KookDebugger.DebugPacket($""" [{DateTimeOffset.Now:HH:mm:ss}] <- [{gatewaySocketFrame.Type}] : #{gatewaySocketFrame.Sequence} [Raw] {raw} - [Parsed] {parsed} """); } JsonElement payloadElement = gatewaySocketFrame.Payload ?? EmptyJsonElement; @@ -122,18 +115,14 @@ await _receivedGatewayEvent private async Task OnTextMessage(string message) { - GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(message, _serializerOptions); + GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(message, _serializerOptions.GetTypedTypeInfo()); if (gatewaySocketFrame is null) return; if (KookDebugger.IsDebuggingPacket) { - string parsed = JsonSerializer - .Serialize(gatewaySocketFrame.Payload, _debugJsonSerializerOptions) - .TrimEnd('\n'); KookDebugger.DebugPacket($""" [{DateTimeOffset.Now:HH:mm:ss}] <- [{gatewaySocketFrame.Type}] : #{gatewaySocketFrame.Sequence} [Raw] {message} - [Parsed] {parsed} """); } JsonElement payloadElement = gatewaySocketFrame.Payload ?? EmptyJsonElement; @@ -287,7 +276,7 @@ await RequestQueue await _sentGatewayMessageEvent.InvokeAsync(gatewaySocketFrameType).ConfigureAwait(false); if (KookDebugger.IsDebuggingPacket) { - string payloadString = JsonSerializer.Serialize(payload, _debugJsonSerializerOptions); + string payloadString = JsonSerializer.Serialize(payload, _serializerOptions.GetTypedTypeInfo()); KookDebugger.DebugPacket( $"[{DateTimeOffset.Now:HH:mm:ss}] -> [{gatewaySocketFrameType}] : #{sequence} \n{payloadString}".TrimEnd('\n')); } diff --git a/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs b/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs index 5571d7bf..b8be7d8b 100644 --- a/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs +++ b/src/Kook.Net.WebSocket/KookSocketClient.Messages.cs @@ -4,7 +4,6 @@ using Kook.API; using Kook.API.Gateway; using Kook.API.Rest; -using Kook.Audio; using Kook.Net; using Kook.Rest; @@ -2100,14 +2099,14 @@ private Cacheable GetCacheableMessage(SocketMessage? value, private T? DeserializePayload(JsonElement jsonElement) { - if (jsonElement.Deserialize(_serializerOptions) is { } x) return x; + if (jsonElement.Deserialize(_serializerOptions.GetTypedTypeInfo()) is { } x) return x; string payloadJson = SerializePayload(jsonElement); _gatewayLogger.ErrorAsync($"Failed to deserialize JSON element to type {typeof(T).Name}: {payloadJson}"); return default; } - private string SerializePayload(object jsonElement) => - JsonSerializer.Serialize(jsonElement, _serializerOptions); + private string SerializePayload(T jsonElement) => + JsonSerializer.Serialize(jsonElement, _serializerOptions.GetTypedTypeInfo()); #endregion diff --git a/src/Kook.Net.WebSocket/KookSocketClient.cs b/src/Kook.Net.WebSocket/KookSocketClient.cs index 339f1545..838a1671 100644 --- a/src/Kook.Net.WebSocket/KookSocketClient.cs +++ b/src/Kook.Net.WebSocket/KookSocketClient.cs @@ -3,12 +3,12 @@ using System.Net; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; using Kook.API; using Kook.API.Gateway; using Kook.API.Rest; using Kook.Logging; using Kook.Net; +using Kook.Net.Contexts; using Kook.Net.Converters; using Kook.Net.Queue; using Kook.Net.Udp; @@ -137,12 +137,13 @@ internal KookSocketClient(KookSocketConfig config, KookSocketApiClient client) _nextAudioId = 1; - _serializerOptions = new JsonSerializerOptions + _serializerOptions = new JsonSerializerOptions(KookWebSocketJsonSerializerContext.Default.Options) { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString, Converters = { CardConverterFactory.Instance } }; + // Also add Rest context for API models + _serializerOptions.TypeInfoResolverChain.Insert(0, KookRestJsonSerializerContext.Default); ApiClient.SentGatewayMessage += async socketFrameType => await _gatewayLogger.DebugAsync($"Sent {socketFrameType}").ConfigureAwait(false); @@ -529,7 +530,7 @@ await _gatewayLogger case MessageTypeGroup: { GatewayEvent? gatewayEvent = - payload.Deserialize>(_serializerOptions); + payload.Deserialize(_serializerOptions.GetTypedTypeInfo>()); if (gatewayEvent is null) { await _gatewayLogger @@ -543,7 +544,7 @@ await _gatewayLogger case MessageTypePerson: { GatewayEvent? gatewayEvent = - payload.Deserialize>(_serializerOptions); + payload.Deserialize(_serializerOptions.GetTypedTypeInfo>()); if (gatewayEvent is null) { await _gatewayLogger @@ -567,7 +568,7 @@ await _gatewayLogger case MessageType.System: { GatewayEvent? gatewayEvent = - payload.Deserialize>(_serializerOptions); + payload.Deserialize(_serializerOptions.GetTypedTypeInfo>()); if (gatewayEvent is not { ExtraData: { } extraData }) { await _gatewayLogger diff --git a/src/Kook.Net.WebSocket/Net/Contexts/KookWebSocketJsonSerializerContext.cs b/src/Kook.Net.WebSocket/Net/Contexts/KookWebSocketJsonSerializerContext.cs new file mode 100644 index 00000000..dadf5a84 --- /dev/null +++ b/src/Kook.Net.WebSocket/Net/Contexts/KookWebSocketJsonSerializerContext.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; +using Kook.API.Gateway; + +namespace Kook.Net.Contexts; + +/// +/// Provides JSON serialization context for Gateway models for Native AOT compatibility. +/// +[JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] +// Gateway Models +[JsonSerializable(typeof(ChannelBatchDeleteEventItem))] +[JsonSerializable(typeof(ChannelBatchUpdateEvent))] +[JsonSerializable(typeof(ChannelDeleteEvent))] +[JsonSerializable(typeof(ChannelSortEvent))] +[JsonSerializable(typeof(DirectMessageButtonClickEvent))] +[JsonSerializable(typeof(DirectMessageDeleteEvent))] +[JsonSerializable(typeof(DirectMessageUpdateEvent))] +[JsonSerializable(typeof(EmbedsAppendEvent))] +[JsonSerializable(typeof(GatewayGroupMessageExtraData))] +[JsonSerializable(typeof(GatewayHelloPayload))] +[JsonSerializable(typeof(GatewayPersonMessageExtraData))] +[JsonSerializable(typeof(GatewayReconnectPayload))] +[JsonSerializable(typeof(GatewaySocketFrame))] +[JsonSerializable(typeof(GatewaySocketFrameType))] +[JsonSerializable(typeof(GatewaySystemEventExtraData))] +[JsonSerializable(typeof(GuildBanEvent))] +[JsonSerializable(typeof(GuildEmojiEvent))] +[JsonSerializable(typeof(GuildEvent))] +[JsonSerializable(typeof(GuildMemberAddEvent))] +[JsonSerializable(typeof(GuildMemberOnlineOfflineEvent))] +[JsonSerializable(typeof(GuildMemberRemoveEvent))] +[JsonSerializable(typeof(GuildMemberUpdateEvent))] +[JsonSerializable(typeof(GuildMuteDeafEvent))] +[JsonSerializable(typeof(GuildUpdateSelfEvent))] +[JsonSerializable(typeof(KMarkdownInfo))] +[JsonSerializable(typeof(LiveInfo))] +[JsonSerializable(typeof(LiveStatusChangeEvent))] +[JsonSerializable(typeof(LiveStreamUser))] +[JsonSerializable(typeof(MessageButtonClickEvent))] +[JsonSerializable(typeof(MessageDeleteEvent))] +[JsonSerializable(typeof(MessagePinEvent))] +[JsonSerializable(typeof(MessageUpdateEvent))] +[JsonSerializable(typeof(PrivateReaction))] +[JsonSerializable(typeof(Reaction))] +[JsonSerializable(typeof(SelfGuildEvent))] +[JsonSerializable(typeof(UserUpdateEvent))] +[JsonSerializable(typeof(UserVoiceEvent))] +// Gateway Events +[JsonSerializable(typeof(GatewayEvent))] +[JsonSerializable(typeof(GatewayEvent))] +[JsonSerializable(typeof(GatewayEvent))] +internal partial class KookWebSocketJsonSerializerContext : JsonSerializerContext; diff --git a/src/Kook.Net.WebSocket/Net/Converters/NullableChannelConverter.cs b/src/Kook.Net.WebSocket/Net/Converters/NullableChannelConverter.cs index 0b020976..f20c9409 100644 --- a/src/Kook.Net.WebSocket/Net/Converters/NullableChannelConverter.cs +++ b/src/Kook.Net.WebSocket/Net/Converters/NullableChannelConverter.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Kook.API; +using Kook.Rest; namespace Kook.Net.Converters; @@ -12,7 +13,7 @@ internal class NullableChannelConverter : JsonConverter switch (reader.TokenType) { case JsonTokenType.StartObject: - return JsonSerializer.Deserialize(ref reader, options); + return JsonSerializer.Deserialize(ref reader, options.GetTypedTypeInfo()); case JsonTokenType.Null or JsonTokenType.StartArray: reader.Skip(); return null; @@ -30,6 +31,6 @@ public override void Write(Utf8JsonWriter writer, Channel? value, JsonSerializer return; } - JsonSerializer.Serialize(writer, value, options); + JsonSerializer.Serialize(writer, value, options.GetTypedTypeInfo()); } } diff --git a/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.Keyed.cs b/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.Keyed.cs index 7e7db044..7989cf05 100644 --- a/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.Keyed.cs +++ b/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.Keyed.cs @@ -1,4 +1,5 @@ -using Kook.Net.DependencyInjection.Microsoft; +using System.Diagnostics.CodeAnalysis; +using Kook.Net.DependencyInjection.Microsoft; using Kook.Net.Webhooks.AspNet; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; @@ -20,6 +21,7 @@ public static partial class KookWebhookClientExtension /// 服务的键。 /// 用于配置 KOOK ASP.NET Webhook 客户端的配置。 /// 添加了 KOOK ASP.NET Webhook 客户端的服务集合。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IServiceCollection AddKeyedKookAspNetWebhookClient(this IServiceCollection services, string? serviceKey, KookAspNetWebhookConfig config) { services.AddKeyedSingleton(serviceKey, config); @@ -35,6 +37,7 @@ public static IServiceCollection AddKeyedKookAspNetWebhookClient(this IServiceCo /// 要向其添加 KOOK ASP.NET Webhook 客户端的服务集合。 /// 服务的键。 /// 添加了 KOOK ASP.NET Webhook 客户端的服务集合。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IServiceCollection AddKeyedKookAspNetWebhookClient(this IServiceCollection services, string? serviceKey) { services.AddKeyedKookWebhookClient(serviceKey, (provider, key) => diff --git a/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.cs b/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.cs index d2c83a77..819beab4 100644 --- a/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.cs +++ b/src/Kook.Net.Webhook.AspNet/KookWebhookClientExtension.cs @@ -15,11 +15,12 @@ namespace Kook.Webhook.AspNet; public static partial class KookWebhookClientExtension { /// - /// 向指定的 添加 客户端。 + /// 向指定的 添加 客户端。 /// /// 要向其添加 KOOK ASP.NET Webhook 客户端的服务集合。 /// 用于配置 KOOK ASP.NET Webhook 客户端的配置。 /// 添加了 KOOK ASP.NET Webhook 客户端的服务集合。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollection services, KookAspNetWebhookConfig config) { services.AddSingleton(config); @@ -35,6 +36,7 @@ public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollect /// 要向其添加 KOOK ASP.NET Webhook 客户端的服务集合。 /// 用于配置 KOOK ASP.NET Webhook 客户端的配置委托。 /// 添加了 KOOK ASP.NET Webhook 客户端的服务集合。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollection services, Action configure) { services.AddKookWebhookClient((_, config) => new KookAspNetWebhookClient(config), configure); @@ -48,6 +50,7 @@ public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollect /// /// 要向其添加 KOOK ASP.NET Webhook 客户端的服务集合。 /// 添加了 KOOK ASP.NET Webhook 客户端的服务集合。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollection services) { services.AddKookWebhookClient(provider => @@ -70,6 +73,7 @@ public static IServiceCollection AddKookAspNetWebhookClient(this IServiceCollect /// KOOK 服务配置器。 /// 用于配置 KOOK ASP.NET Webhook 客户端的配置委托。 /// 配置了 KOOK ASP.NET Webhook 客户端的配置器。 + [RequiresUnreferencedCode("MVC does not currently support trimming or native AOT.")] public static IKookClientConfigurator UseAspNetWebhookClient (this IKookClientServiceConfigurator configurator, Action configure) { diff --git a/src/Kook.Net.Webhook/KookWebhookApiClient.cs b/src/Kook.Net.Webhook/KookWebhookApiClient.cs index ea99aa53..4b66472a 100644 --- a/src/Kook.Net.Webhook/KookWebhookApiClient.cs +++ b/src/Kook.Net.Webhook/KookWebhookApiClient.cs @@ -4,12 +4,14 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; using Kook.API.Gateway; using Kook.API.Webhook; +using Kook.Net.Contexts; +using Kook.Net.Converters; using Kook.Net.Rest; using Kook.Net.Webhooks; using Kook.Net.WebSockets; +using Kook.Rest; namespace Kook.API; @@ -23,10 +25,9 @@ public event Func WebhookChallenge private readonly AsyncEvent> _webhookChallenge = new(); - private static readonly JsonSerializerOptions SerializerOptions = new() + private static readonly JsonSerializerOptions SerializerOptions = new(KookWebhookJsonSerializerContext.Default.Options) { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - NumberHandling = JsonNumberHandling.AllowReadingFromString + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; private readonly string _encryptKey; @@ -43,6 +44,7 @@ public KookWebhookApiClient(RestClientProvider restClientProvider, : base(restClientProvider, webSocketProvider, userAgent, acceptLanguage, null, defaultRetryMode, serializerOptions, defaultRatelimitCallback) { + InjectJsonTypeInfos(KookWebhookJsonSerializerContext.Default.Options.TypeInfoResolverChain); _encryptKey = encryptKey; _verifyToken = verifyToken; WebhookClient = webhookProvider(); @@ -75,14 +77,14 @@ public KookWebhookApiClient(RestClientProvider restClientProvider, decompressed.Position = 0; #if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - GatewayEncryptedFrame? gatewayEncryptedFrame = await JsonSerializer.DeserializeAsync(decompressed, _serializerOptions); + GatewayEncryptedFrame? gatewayEncryptedFrame = await JsonSerializer.DeserializeAsync(decompressed, _serializerOptions.GetTypedTypeInfo()); #else - GatewayEncryptedFrame? gatewayEncryptedFrame = JsonSerializer.Deserialize(decompressed, _serializerOptions); + GatewayEncryptedFrame? gatewayEncryptedFrame = JsonSerializer.Deserialize(decompressed, _serializerOptions.GetTypedTypeInfo()); #endif if (gatewayEncryptedFrame is null) return null; string decryptedFrame = Decrypt(gatewayEncryptedFrame.Encrypted, _encryptKey); - GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(decryptedFrame, _serializerOptions); + GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(decryptedFrame, _serializerOptions.GetTypedTypeInfo()); if (gatewaySocketFrame is null) return null; JsonElement payloadElement = gatewaySocketFrame.Payload ?? EmptyJsonElement; @@ -91,7 +93,7 @@ public KookWebhookApiClient(RestClientProvider restClientProvider, if (TryParseWebhookChallenge(gatewaySocketFrame.Type, payloadElement, out string? challenge)) { await _webhookChallenge.InvokeAsync(challenge); - return JsonSerializer.Serialize(new GatewayChallengeFrame { Challenge = challenge }, SerializerOptions); + return JsonSerializer.Serialize(new GatewayChallengeFrame { Challenge = challenge }, SerializerOptions.GetTypedTypeInfo()); } await _receivedGatewayEvent @@ -102,11 +104,11 @@ await _receivedGatewayEvent private async Task OnTextMessage(string message) { - GatewayEncryptedFrame? gatewayEncryptedFrame = JsonSerializer.Deserialize(message, _serializerOptions); + GatewayEncryptedFrame? gatewayEncryptedFrame = JsonSerializer.Deserialize(message, _serializerOptions.GetTypedTypeInfo()); if (gatewayEncryptedFrame is null) return null; string decryptedFrame = Decrypt(gatewayEncryptedFrame.Encrypted, _encryptKey); - GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(decryptedFrame, _serializerOptions); + GatewaySocketFrame? gatewaySocketFrame = JsonSerializer.Deserialize(decryptedFrame, _serializerOptions.GetTypedTypeInfo()); if (gatewaySocketFrame is null) return null; JsonElement payloadElement = gatewaySocketFrame.Payload ?? EmptyJsonElement; @@ -115,7 +117,7 @@ await _receivedGatewayEvent if (TryParseWebhookChallenge(gatewaySocketFrame.Type, payloadElement, out string? challenge)) { await _webhookChallenge.InvokeAsync(challenge); - return JsonSerializer.Serialize(new GatewayChallengeFrame { Challenge = challenge }, SerializerOptions); + return JsonSerializer.Serialize(new GatewayChallengeFrame { Challenge = challenge }, SerializerOptions.GetTypedTypeInfo()); } await _receivedGatewayEvent diff --git a/src/Kook.Net.Webhook/Net/Converters/KookWebhookJsonSerializerContext.cs b/src/Kook.Net.Webhook/Net/Converters/KookWebhookJsonSerializerContext.cs new file mode 100644 index 00000000..555c80a3 --- /dev/null +++ b/src/Kook.Net.Webhook/Net/Converters/KookWebhookJsonSerializerContext.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using Kook.API.Webhook; + +namespace Kook.Net.Contexts; + +/// +/// Provides JSON serialization context for Webhook models for Native AOT compatibility. +/// +[JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] +[JsonSerializable(typeof(GatewayChallengeFrame))] +[JsonSerializable(typeof(GatewayEncryptedFrame))] +internal partial class KookWebhookJsonSerializerContext : JsonSerializerContext; diff --git a/test/Kook.Net.Tests.Unit/CardBuilderTests.cs b/test/Kook.Net.Tests.Unit/CardBuilderTests.cs index dd15a84c..9f0b4977 100644 --- a/test/Kook.Net.Tests.Unit/CardBuilderTests.cs +++ b/test/Kook.Net.Tests.Unit/CardBuilderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Xunit; namespace Kook;