< Summary

Information
Class: TeleFlow.Telegram.Internal.Handlers.TelegramCustomFilterCallSite
Assembly: TeleFlow.Framework
File(s): /_/src/TeleFlow.Framework/Internal/Handlers/TelegramCustomFilterCallSite.cs
Line coverage
86%
Covered lines: 72
Uncovered lines: 11
Coverable lines: 83
Total lines: 237
Line coverage: 86.7%
Branch coverage
65%
Covered branches: 26
Total branches: 40
Branch coverage: 65%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
Create(...)75%8886.66%
MatchesAsync(...)100%11100%
ResolveUntypedFilterContextType(...)50%4480%
ResolveTypedFilterContextType(...)50%6681.81%
ResolveContextType(...)75%88100%
ValidateConcreteFilterType(...)50%7671.42%
ValidateResolvedContextType(...)75%5466.66%
GetExpectedContextType(...)75%4483.33%
CreateUntypedInvoker(...)100%11100%
CreateTypedInvoker(...)100%11100%
InvokeUntypedFilterAsync(...)100%11100%
InvokeTypedFilterAsync(...)100%11100%

File(s)

/_/src/TeleFlow.Framework/Internal/Handlers/TelegramCustomFilterCallSite.cs

#LineLine coverage
 1using System.Reflection;
 2using Microsoft.Extensions.DependencyInjection;
 3
 4namespace TeleFlow.Telegram.Internal.Handlers;
 5
 6/// <summary>
 7/// Stores a validated custom filter invocation path prepared while the handler table is built.
 8/// Incoming updates reuse this call site so custom filter evaluation does not discover interfaces,
 9/// inspect attributes, or create reflection calls on the dispatcher hot path.
 10/// </summary>
 11internal sealed class TelegramCustomFilterCallSite
 12{
 113    private static readonly MethodInfo UntypedInvokerMethod = typeof(TelegramCustomFilterCallSite)
 114        .GetMethod(nameof(InvokeUntypedFilterAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
 15
 116    private static readonly MethodInfo TypedInvokerMethod = typeof(TelegramCustomFilterCallSite)
 117        .GetMethod(nameof(InvokeTypedFilterAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
 18
 19    private readonly TelegramCustomFilterInvoker _invoker;
 20
 21    private TelegramCustomFilterCallSite(
 22        Type filterType,
 23        Attribute? attribute,
 24        TelegramCustomFilterInvoker invoker)
 25    {
 26        FilterType = filterType;
 27        Attribute = attribute;
 6128        _invoker = invoker;
 6129    }
 30
 31    public Type FilterType { get; }
 32
 33    public Attribute? Attribute { get; }
 34
 35    public static TelegramCustomFilterCallSite Create(
 36        Type filterType,
 37        Type? contextType,
 38        Attribute? attribute,
 39        TelegramHandlerKind handlerKind)
 40    {
 6141        ValidateConcreteFilterType(filterType);
 42
 6143        var expectedContextType = GetExpectedContextType(handlerKind);
 44
 6145        if (attribute is null)
 46        {
 3647            var resolvedContextType = contextType ?? ResolveUntypedFilterContextType(filterType, expectedContextType);
 48
 3649            ValidateResolvedContextType(filterType, resolvedContextType, expectedContextType);
 50
 3651            var invoker = CreateUntypedInvoker(resolvedContextType);
 52
 3653            return new TelegramCustomFilterCallSite(filterType, attribute: null, invoker);
 54        }
 55
 2556        var attributeType = attribute.GetType();
 57
 2558        if (attributeType.IsGenericType)
 59        {
 060            throw new InvalidOperationException(
 061                $"Custom Telegram filter attribute type '{attributeType.FullName}' must be non-generic.");
 62        }
 63
 2564        var typedContextType = contextType ?? ResolveTypedFilterContextType(filterType, expectedContextType, attributeTy
 65
 2566        ValidateResolvedContextType(filterType, typedContextType, expectedContextType);
 67
 2568        var typedInvoker = CreateTypedInvoker(typedContextType, attributeType);
 69
 2570        return new TelegramCustomFilterCallSite(filterType, attribute, typedInvoker);
 71    }
 72
 73    public ValueTask<bool> MatchesAsync(
 74        TelegramUpdateContext context,
 75        CancellationToken cancellationToken)
 76    {
 2977        var filter = context.Services.GetRequiredService(FilterType);
 2878        return _invoker(filter, context, Attribute, cancellationToken);
 79    }
 80
 81    private static Type ResolveUntypedFilterContextType(
 82        Type filterType,
 83        Type expectedContextType)
 84    {
 3685        var contextType = ResolveContextType(
 3686            filterType,
 3687            expectedContextType,
 3688            static type => type.IsGenericType &&
 3689                           type.GetGenericTypeDefinition() == typeof(ITelegramFilter<>),
 7290            static type => type.GetGenericArguments()[0]);
 91
 3692        if (contextType is null)
 93        {
 094            throw new InvalidOperationException(
 095                $"Custom Telegram filter type '{filterType.FullName}' must implement ITelegramFilter<TContext> compatibl
 96        }
 97
 3698        return contextType;
 99    }
 100
 101    private static Type ResolveTypedFilterContextType(
 102        Type filterType,
 103        Type expectedContextType,
 104        Type attributeType)
 105    {
 1106        var contextType = ResolveContextType(
 1107            filterType,
 1108            expectedContextType,
 1109            type => type.IsGenericType &&
 1110                    type.GetGenericTypeDefinition() == typeof(ITelegramFilter<,>) &&
 1111                    type.GetGenericArguments()[1] == attributeType,
 2112            static type => type.GetGenericArguments()[0]);
 113
 1114        if (contextType is null)
 115        {
 0116            throw new InvalidOperationException(
 0117                $"Parameterized custom Telegram filter type '{filterType.FullName}' must implement ITelegramFilter<TCont
 118        }
 119
 1120        return contextType;
 121    }
 122
 123    private static Type? ResolveContextType(
 124        Type filterType,
 125        Type expectedContextType,
 126        Func<Type, bool> interfacePredicate,
 127        Func<Type, Type> contextSelector)
 128    {
 37129        Type? updateContextType = null;
 130
 112131        foreach (var interfaceType in filterType.GetInterfaces())
 132        {
 37133            if (!interfacePredicate(interfaceType))
 134            {
 135                continue;
 136            }
 137
 37138            var contextType = contextSelector(interfaceType);
 139
 37140            if (contextType == expectedContextType)
 141            {
 36142                return contextType;
 143            }
 144
 1145            if (contextType == typeof(TelegramUpdateContext))
 146            {
 1147                updateContextType = contextType;
 148            }
 149        }
 150
 1151        return updateContextType;
 152    }
 153
 154    private static void ValidateConcreteFilterType(Type filterType)
 155    {
 61156        ArgumentNullException.ThrowIfNull(filterType);
 157
 61158        if (filterType.IsInterface ||
 61159            filterType.IsAbstract ||
 61160            filterType.ContainsGenericParameters)
 161        {
 0162            throw new InvalidOperationException(
 0163                $"Custom Telegram filter type '{filterType.FullName}' must be a concrete closed type.");
 164        }
 61165    }
 166
 167    private static void ValidateResolvedContextType(
 168        Type filterType,
 169        Type contextType,
 170        Type expectedContextType)
 171    {
 61172        ArgumentNullException.ThrowIfNull(contextType);
 173
 61174        if (contextType != expectedContextType &&
 61175            contextType != typeof(TelegramUpdateContext))
 176        {
 0177            throw new InvalidOperationException(
 0178                $"Custom Telegram filter type '{filterType.FullName}' is not compatible with {expectedContextType.Name} 
 179        }
 61180    }
 181
 182    private static Type GetExpectedContextType(TelegramHandlerKind handlerKind)
 183    {
 61184        return handlerKind switch
 61185        {
 2186            TelegramHandlerKind.Callback => typeof(CallbackQueryContext),
 0187            TelegramHandlerKind.ChatMember => typeof(ChatMemberUpdatedContext),
 59188            _ => typeof(MessageContext)
 61189        };
 190    }
 191
 192    private static TelegramCustomFilterInvoker CreateUntypedInvoker(Type contextType)
 193    {
 36194        return (TelegramCustomFilterInvoker)UntypedInvokerMethod
 36195            .MakeGenericMethod(contextType)
 36196            .CreateDelegate(typeof(TelegramCustomFilterInvoker));
 197    }
 198
 199    private static TelegramCustomFilterInvoker CreateTypedInvoker(
 200        Type contextType,
 201        Type attributeType)
 202    {
 25203        return (TelegramCustomFilterInvoker)TypedInvokerMethod
 25204            .MakeGenericMethod(contextType, attributeType)
 25205            .CreateDelegate(typeof(TelegramCustomFilterInvoker));
 206    }
 207
 208    private static ValueTask<bool> InvokeUntypedFilterAsync<TContext>(
 209        object filter,
 210        TelegramUpdateContext context,
 211        Attribute? attribute,
 212        CancellationToken cancellationToken)
 213        where TContext : TelegramUpdateContext
 214    {
 10215        return ((ITelegramFilter<TContext>)filter).MatchesAsync((TContext)context, cancellationToken);
 216    }
 217
 218    private static ValueTask<bool> InvokeTypedFilterAsync<TContext, TAttribute>(
 219        object filter,
 220        TelegramUpdateContext context,
 221        Attribute? attribute,
 222        CancellationToken cancellationToken)
 223        where TContext : TelegramUpdateContext
 224        where TAttribute : Attribute
 225    {
 18226        return ((ITelegramFilter<TContext, TAttribute>)filter).MatchesAsync(
 18227            (TContext)context,
 18228            (TAttribute)attribute!,
 18229            cancellationToken);
 230    }
 231
 232    private delegate ValueTask<bool> TelegramCustomFilterInvoker(
 233        object filter,
 234        TelegramUpdateContext context,
 235        Attribute? attribute,
 236        CancellationToken cancellationToken);
 237}