| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Reflection; |
| | | 3 | | using System.Text.Json.Serialization; |
| | | 4 | | |
| | | 5 | | namespace TeleFlow.Telegram.Internal; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Caches reflection metadata used by Telegram request content building. |
| | | 9 | | /// The cache keeps JSON property discovery, ignored-property handling, and scalar detection in one place. |
| | | 10 | | /// It is shared by JSON/multipart routing and multipart rendering so both paths interpret generated schema types consis |
| | | 11 | | /// </summary> |
| | | 12 | | internal static class TelegramRequestTypeMetadataCache |
| | | 13 | | { |
| | | 14 | | private static readonly ConcurrentDictionary<Type, TelegramRequestTypeMetadata> MetadataCache = new(); |
| | | 15 | | |
| | | 16 | | public static TelegramRequestTypeMetadata Get(Type type) |
| | | 17 | | { |
| | | 18 | | ArgumentNullException.ThrowIfNull(type); |
| | | 19 | | return MetadataCache.GetOrAdd(type, Create); |
| | | 20 | | } |
| | | 21 | | |
| | | 22 | | public static bool IsScalarType(Type type) |
| | | 23 | | { |
| | | 24 | | ArgumentNullException.ThrowIfNull(type); |
| | | 25 | | |
| | | 26 | | var scalarType = Nullable.GetUnderlyingType(type) ?? type; |
| | | 27 | | |
| | | 28 | | return scalarType.IsPrimitive || |
| | | 29 | | scalarType.IsEnum || |
| | | 30 | | scalarType == typeof(string) || |
| | | 31 | | scalarType == typeof(decimal) || |
| | | 32 | | scalarType == typeof(DateTime) || |
| | | 33 | | scalarType == typeof(DateTimeOffset) || |
| | | 34 | | scalarType == typeof(Guid); |
| | | 35 | | } |
| | | 36 | | |
| | | 37 | | private static TelegramRequestTypeMetadata Create(Type type) |
| | | 38 | | { |
| | | 39 | | var readableProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) |
| | | 40 | | .Where(static property => property.CanRead && property.GetIndexParameters().Length == 0) |
| | | 41 | | // Keep reflection metadata aligned with System.Text.Json and avoid evaluating ignored getters |
| | | 42 | | // while deciding whether a request needs multipart. |
| | | 43 | | .Where(static property => !IsAlwaysJsonIgnored(property)) |
| | | 44 | | .Select(static property => new TelegramRequestPropertyMetadata( |
| | | 45 | | property, |
| | | 46 | | property.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name)) |
| | | 47 | | .ToArray(); |
| | | 48 | | |
| | | 49 | | var telegramProperties = readableProperties |
| | | 50 | | .Where(static property => property.JsonPropertyName is not null) |
| | | 51 | | .ToArray(); |
| | | 52 | | |
| | | 53 | | return new TelegramRequestTypeMetadata(IsScalarType(type), readableProperties, telegramProperties); |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | private static bool IsAlwaysJsonIgnored(PropertyInfo property) |
| | | 57 | | { |
| | | 58 | | var ignore = property.GetCustomAttribute<JsonIgnoreAttribute>(); |
| | | 59 | | return ignore?.Condition == JsonIgnoreCondition.Always; |
| | | 60 | | } |
| | | 61 | | } |
| | | 62 | | |
| | | 63 | | /// <summary> |
| | | 64 | | /// Reflection snapshot for one request-related CLR type. |
| | | 65 | | /// It separates all readable properties from Telegram JSON fields discovered through JsonPropertyName metadata. |
| | | 66 | | /// </summary> |
| | | 67 | | internal sealed record TelegramRequestTypeMetadata( |
| | | 68 | | bool IsScalar, |
| | | 69 | | TelegramRequestPropertyMetadata[] ReadableProperties, |
| | | 70 | | TelegramRequestPropertyMetadata[] TelegramProperties); |
| | | 71 | | |
| | | 72 | | /// <summary> |
| | | 73 | | /// Reflection wrapper around a readable CLR property and its optional Telegram JSON field name. |
| | | 74 | | /// Multipart rendering uses it to read CLR values and write Telegram field names without repeating reflection rules. |
| | | 75 | | /// </summary> |
| | | 76 | | internal sealed record TelegramRequestPropertyMetadata(PropertyInfo Property, string? JsonPropertyName) |
| | | 77 | | { |
| | 254 | 78 | | public Type PropertyType => Property.PropertyType; |
| | | 79 | | |
| | 69 | 80 | | public string Name => Property.Name; |
| | | 81 | | |
| | | 82 | | public string TelegramName => |
| | 27 | 83 | | JsonPropertyName ?? |
| | 27 | 84 | | throw new InvalidOperationException( |
| | 27 | 85 | | $"Telegram payload property '{Property.DeclaringType?.FullName}.{Property.Name}' is missing JsonPropertyName |
| | | 86 | | |
| | | 87 | | public object? GetValue(object value) |
| | | 88 | | { |
| | 326 | 89 | | return Property.GetValue(value); |
| | | 90 | | } |
| | | 91 | | } |