< Summary

Information
Class: NanoRoute.HandlerExtensions.NanoRouteHandlerExtensions
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/NanoRouteHandlerExtensions.cs
Line coverage
96%
Covered lines: 76
Uncovered lines: 3
Coverable lines: 79
Total lines: 343
Line coverage: 96.2%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/NanoRouteHandlerExtensions.cs

#LineLine coverage
 1/********************************************************************************
 2* NanoRouteHandlerExtensions.cs                                                 *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.Diagnostics;
 9using System.Diagnostics.CodeAnalysis;
 10using System.Linq.Expressions;
 11using System.Net.Http;
 12using System.Reflection;
 13using System.Runtime.CompilerServices;
 14using System.Threading;
 15using System.Threading.Tasks;
 16
 17using Microsoft.Extensions.DependencyInjection;
 18
 19namespace NanoRoute.HandlerExtensions
 20{
 21    using Internals;
 22    using Properties;
 23
 24    /// <summary>
 25    /// Describes how a typed handler property is populated.
 26    /// </summary>
 27    public enum ValueSource
 28    {
 29        /// <summary>
 30        /// Leaves the property untouched.
 31        /// </summary>
 32        Skip,
 33
 34        /// <summary>
 35        /// Reads the value from <see cref="RequestContext.Parameters"/>.
 36        /// </summary>
 37        Context,
 38
 39        /// <summary>
 40        /// Resolves the value from <see cref="RequestContext.Services"/>.
 41        /// </summary>
 42        ServiceLocator
 43    }
 44
 45    /// <summary>
 46    /// Overrides the default binding behavior for a typed handler request property.
 47    /// </summary>
 48    /// <param name="source">The source used to populate the annotated property.</param>
 49    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
 50    public sealed class ValueSourceAttribute(ValueSource source) : Attribute
 51    {
 52        /// <summary>
 53        /// Gets the binding source used for the annotated property.
 54        /// </summary>
 55        public ValueSource Source { get; } = source;
 56
 57        /// <summary>
 58        /// Gets or sets an optional binding name.
 59        /// </summary>
 60        /// <remarks>
 61        /// For <see cref="ValueSource.Context"/>, this overrides the key looked up in
 62        /// <see cref="RequestContext.Parameters"/>. For <see cref="ValueSource.ServiceLocator"/>,
 63        /// this is treated as the keyed service name. <see cref="ValueSource.Skip"/> does not allow
 64        /// a name because no value is read.
 65        /// </remarks>
 66        public string? Name
 67        {
 68            get;
 69            init
 70            {
 71                if (Source is ValueSource.Skip && value is not null)
 72                    throw new InvalidOperationException(Resources.ERR_SKIPPED_VALUE_SOURCE_NAME);
 73
 74                field = value;
 75            }
 76        }
 77    }
 78
 79    /// <summary>
 80    /// Adds typed handler overloads that project a <see cref="RequestContext"/> into a request object.
 81    /// </summary>
 82    /// <remarks>
 83    /// <para>
 84    /// By default, writable public properties are bound from <see cref="RequestContext.Parameters"/>
 85    /// using the property name as the lookup key.
 86    /// </para>
 87    /// <para>
 88    /// Properties of type <see cref="RequestContext"/> and <see cref="CancellationToken"/> are populated
 89    /// automatically from the current request.
 90    /// </para>
 91    /// <para>
 92    /// Use <see cref="ValueSourceAttribute"/> to bind a property from a specific context key or service.
 93    /// Missing required context values and services throw <see cref="InvalidOperationException"/>.
 94    /// </para>
 95    /// </remarks>
 96    public static class NanoRouteHandlerExtensions
 97    {
 98        private static Func<RequestContext, TRequestContext> CreateContextMapperDelegate<[DynamicallyAccessedMembers(Dyn
 199        {
 1100            ParameterExpression
 1101                source = Expression.Parameter(typeof(RequestContext), nameof(source)),
 1102                result = Expression.Variable(typeof(TRequestContext), nameof(result));
 103
 1104            List<Expression> propSetters =
 1105            [
 1106                Expression.Assign
 1107                (
 1108                    result,
 1109                    Expression.New
 1110                    (
 1111                        typeof(TRequestContext).GetConstructor(Type.EmptyTypes)
 1112                    )
 1113                )
 1114            ];
 115
 1116            foreach (PropertyInfo prop in typeof(TRequestContext).GetProperties(BindingFlags.Instance | BindingFlags.Pub
 1117            {
 1118                if (!prop.CanWrite)
 1119                    continue;
 120
 1121                ValueSourceAttribute? valueSource = prop.GetCustomAttribute<ValueSourceAttribute>();
 122
 1123                switch (valueSource?.Source)
 124                {
 125                    case ValueSource.Skip:
 1126                        continue;
 127                    case ValueSource.ServiceLocator:
 1128                    {
 1129                        SetProperty
 1130                        (
 1131                            valueSource?.Name is { } name
 1132                                ? context => context.Services.GetRequiredKeyedService(prop.PropertyType, name)
 1133                                : context => context.Services.GetRequiredService(prop.PropertyType)
 1134                        );
 1135                        continue;
 136                    }
 137                    case ValueSource.Context:
 1138                    {
 1139                        string name = valueSource?.Name ?? prop.Name;
 1140                        SetProperty
 1141                        (
 1142                            context => context.Parameters.TryGetValue(name, out object? arg)
 1143                                ? arg!
 1144                                // InvalidOperationException may map to HTTP 500 but the developer message will contain 
 1145                                : throw new InvalidOperationException(string.Format(Resources.Culture, Resources.ERR_MIS
 1146                        );
 1147                        continue;
 148                    }
 149                    case null:
 1150                        switch (prop.PropertyType)
 151                        {
 1152                            case Type x when x == typeof(RequestContext):
 1153                                SetPropertyValue(source);
 1154                                continue;
 1155                            case Type x when x == typeof(CancellationToken):
 1156                                SetProperty(static context => context.Cancellation);
 1157                                continue;
 158                        }
 1159                        goto case ValueSource.Context;
 160                    default:
 0161                        Debug.Fail($"Unknown source: {valueSource.Source}");
 0162                        break;
 163                }
 164
 165                void SetPropertyValue(Expression value) => propSetters.Add
 166                (
 167                    Expression.Assign
 168                    (
 169                        Expression.Property(result, prop),
 170                        Expression.Convert(value, prop.PropertyType)
 171                    )
 172                );
 173
 174                void SetProperty(Func<RequestContext, object> del) => SetPropertyValue
 175                (
 176                    Expression.Invoke
 177                    (
 178                        Expression.Constant(del, typeof(Func<RequestContext, object>)),
 179                        source
 180                    )
 181                );
 0182            }
 183
 1184            propSetters.Add(result);  // return result;
 185
 186            // In native AOT context this will be interpreted rather than compiled
 1187            return Expression.Lambda<Func<RequestContext, TRequestContext>>(Expression.Block([result], propSetters), sou
 1188            (
 1189                preferInterpretation: !RuntimeFeature.IsDynamicCodeSupported
 1190            );
 1191        }
 192
 193        private static TBuilder AddHandlerCore<TBuilder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Publ
 1194        {
 1195            Ensure.NotNull(routeBuilder);
 1196            Ensure.NotNull(verbs);
 1197            Ensure.NotNull(pattern);
 1198            Ensure.NotNull(handler);
 199
 1200            if (queryBindings is not null)
 1201                routeBuilder.AddQueryBindings(verbs, pattern, queryBindings);
 202
 1203            Func<RequestContext, TRequestContext> mapContext = CreateContextMapperDelegate<TRequestContext>();
 204
 1205            routeBuilder.AddHandler(verbs, pattern, (context, next) => handler(mapContext(context), next));
 206
 1207            return routeBuilder;
 1208        }
 209
 210        extension<TBuilder>(TBuilder routeBuilder) where TBuilder : RouteBuilder
 211        {
 212            /// <summary>
 213            /// Registers a typed handler that receives a request object built from the current <see cref="RequestContex
 214            /// </summary>
 215            /// <typeparam name="TRequestContext">
 216            /// The request-object type populated from the current route parameters, query bindings, services, and speci
 217            /// </typeparam>
 218            /// <param name="verbs">The HTTP verbs handled by the route.</param>
 219            /// <param name="pattern">The route pattern to register.</param>
 220            /// <param name="handler">The typed handler delegate.</param>
 221            /// <returns>The current <paramref name="routeBuilder"/>.</returns>
 222            /// <remarks>
 223            /// <para>
 224            /// Writable public properties are bound from <see cref="RequestContext.Parameters"/> by default.
 225            /// </para>
 226            /// <para>
 227            /// A property of type <see cref="RequestContext"/> receives the current context, and a property of type
 228            /// <see cref="CancellationToken"/> receives the active request token.
 229            /// </para>
 230            /// <para>
 231            /// Apply <see cref="ValueSourceAttribute"/> to bind a property from a different parameter name
 232            /// or from the request service provider.
 233            /// </para>
 234            /// </remarks>
 235            public TBuilder AddHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | Dyn
 1236            {
 1237                Ensure.NotNull(handler);
 1238                return AddHandlerCore(routeBuilder, verbs, pattern, null, (TRequestContext context, CallNextHandlerDeleg
 239            }
 240
 241            /// <summary>
 242            /// Registers a typed middleware handler that receives a request object and the next handler in the pipeline
 243            /// </summary>
 244            /// <typeparam name="TRequestContext">
 245            /// The request-object type populated from the current route parameters, query bindings, services, and speci
 246            /// </typeparam>
 247            /// <param name="verbs">The HTTP verbs handled by the route.</param>
 248            /// <param name="pattern">The route pattern to register.</param>
 249            /// <param name="handler">The typed middleware delegate.</param>
 250            /// <returns>The current <paramref name="routeBuilder"/>.</returns>
 251            /// <remarks>
 252            /// <para>
 253            /// Writable public properties are bound from <see cref="RequestContext.Parameters"/> by default.
 254            /// </para>
 255            /// <para>
 256            /// A property of type <see cref="RequestContext"/> receives the current context, and a property of type
 257            /// <see cref="CancellationToken"/> receives the active request token.
 258            /// </para>
 259            /// <para>
 260            /// Apply <see cref="ValueSourceAttribute"/> to bind a property from a different parameter name
 261            /// or from the request service provider.
 262            /// </para>
 263            /// </remarks>
 264            public TBuilder AddHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | Dyn
 1265                AddHandlerCore(routeBuilder, verbs, pattern, null, handler);
 266
 267            /// <summary>
 268            /// Registers a typed handler and the query-string bindings it depends on.
 269            /// </summary>
 270            /// <typeparam name="TRequestContext">
 271            /// The request-object type populated from the current route parameters, query bindings, services, and speci
 272            /// </typeparam>
 273            /// <param name="verbs">The HTTP verbs handled by the route.</param>
 274            /// <param name="pattern">The route pattern to register.</param>
 275            /// <param name="queryBindings">
 276            /// A query-parameter descriptor that is applied before <paramref name="handler"/> is invoked.
 277            /// </param>
 278            /// <param name="handler">The typed handler delegate.</param>
 279            /// <returns>The current <paramref name="routeBuilder"/>.</returns>
 280            /// <remarks>
 281            /// <para>
 282            /// Writable public properties are bound from <see cref="RequestContext.Parameters"/> by default.
 283            /// </para>
 284            /// <para>
 285            /// A property of type <see cref="RequestContext"/> receives the current context, and a property of type
 286            /// <see cref="CancellationToken"/> receives the active request token.
 287            /// </para>
 288            /// <para>
 289            /// <paramref name="queryBindings"/> are registered before the request object is created, so their parsed va
 290            /// are available through the default context binding rules.
 291            /// </para>
 292            /// <para>
 293            /// Apply <see cref="ValueSourceAttribute"/> to bind a property from a different parameter name
 294            /// or from the request service provider.
 295            /// </para>
 296            /// </remarks>
 297            public TBuilder AddHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | Dyn
 1298            {
 1299                Ensure.NotNull(handler);
 1300                Ensure.NotNull(queryBindings);
 301
 1302                return AddHandlerCore(routeBuilder, verbs, pattern, queryBindings, (TRequestContext context, CallNextHan
 303            }
 304
 305            /// <summary>
 306            /// Registers a typed middleware handler and the query-string bindings it depends on.
 307            /// </summary>
 308            /// <typeparam name="TRequestContext">
 309            /// The request-object type populated from the current route parameters, query bindings, services, and speci
 310            /// </typeparam>
 311            /// <param name="verbs">The HTTP verbs handled by the route.</param>
 312            /// <param name="pattern">The route pattern to register.</param>
 313            /// <param name="queryBindings">
 314            /// A query-parameter descriptor that is applied before <paramref name="handler"/> is invoked.
 315            /// </param>
 316            /// <param name="handler">The typed middleware delegate.</param>
 317            /// <returns>The current <paramref name="routeBuilder"/>.</returns>
 318            /// <remarks>
 319            /// <para>
 320            /// Writable public properties are bound from <see cref="RequestContext.Parameters"/> by default.
 321            /// </para>
 322            /// <para>
 323            /// A property of type <see cref="RequestContext"/> receives the current context, and a property of type
 324            /// <see cref="CancellationToken"/> receives the active request token.
 325            /// </para>
 326            /// <para>
 327            /// <paramref name="queryBindings"/> are registered before the request object is created, so their parsed va
 328            /// are available through the default context binding rules.
 329            /// </para>
 330            /// <para>
 331            /// Apply <see cref="ValueSourceAttribute"/> to bind a property from a different parameter name
 332            /// or from the request service provider.
 333            /// </para>
 334            /// </remarks>
 335            public TBuilder AddHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | Dyn
 1336            {
 1337                Ensure.NotNull(queryBindings);
 338
 1339                return AddHandlerCore(routeBuilder, verbs, pattern, queryBindings, handler);
 340            }
 341        }
 342    }
 343}