Ejemplo n.º 1
0
        [TestCase("/abc/{width}/{height}/")] // 2 parameters, different segments.
        public void ValidBaseRoute_IsValid(string route)
        {
            RouteMatcher.ClearCache();

            Assert.IsTrue(Route.IsValid(route, true));
            Assert.DoesNotThrow(() => RouteMatcher.Parse(route, true));
            Assert.IsTrue(RouteMatcher.TryParse(route, true, out _));
        }
Ejemplo n.º 2
0
        [TestCase("/abc/{id}/{name?}/")]  // No segment of a base route can be optional.
        public void InvalidBaseRoute_IsNotValid(string route)
        {
            RouteMatcher.ClearCache();

            Assert.IsFalse(Route.IsValid(route, true));
            Assert.Throws <FormatException>(() => RouteMatcher.Parse(route, true));
            Assert.IsFalse(RouteMatcher.TryParse(route, true, out _));
        }
Ejemplo n.º 3
0
 public CustomModule(string baseRoute)
 {
     BaseRoute     = Validate.Route(nameof(baseRoute), baseRoute, true);
     _routeMatcher = RouteMatcher.Parse(baseRoute, true);
     LogSource     = GetType().Name;
 }
Ejemplo n.º 4
0
 /// <summary>
 /// Initializes a new instance of the <see cref="WebModuleBase"/> class.
 /// </summary>
 /// <param name="baseRoute">The base route served by this module.</param>
 /// <exception cref="ArgumentNullException"><paramref name="baseRoute"/> is <see langword="null"/>.</exception>
 /// <exception cref="ArgumentException"><paramref name="baseRoute"/> is not a valid base route.</exception>
 /// <seealso cref="IWebModule.BaseRoute"/>
 /// <seealso cref="Validate.Route"/>
 protected WebModuleBase(string baseRoute)
 {
     BaseRoute     = Validate.Route(nameof(baseRoute), baseRoute, true);
     _routeMatcher = RouteMatcher.Parse(baseRoute, true);
     LogSource     = GetType().Name;
 }
Ejemplo n.º 5
0
        // Compile a handler.
        //
        // Parameters:
        // - factoryExpression is an Expression that builds a controller;
        // - method is a MethodInfo for a public instance method of the controller;
        // - route is the route to which the controller method is associated.
        //
        // This method builds a lambda, with the same signature as a RouteHandlerCallback<IHttpContext>, that:
        // - uses factoryExpression to build a controller;
        // - calls the controller method, passing converted route parameters for method parameters with matching names
        //   and default values for other parameters;
        // - serializes the returned object (or the result of the returned task),
        //   unless the return type of the controller method is void or Task;
        // - if the controller implements IDisposable, disposes it.
        private RouteHandlerCallback CompileHandler(Expression factoryExpression, MethodInfo method, string route)
        {
            // Parse the route
            var matcher = RouteMatcher.Parse(route, false);

            // Lambda parameters
            var contextInLambda = Expression.Parameter(typeof(IHttpContext), "context");
            var routeInLambda   = Expression.Parameter(typeof(RouteMatch), "route");

            // Local variables
            var locals = new List <ParameterExpression>();

            // Local variable for controller
            var controllerType = method.ReflectedType;
            var controller     = Expression.Variable(controllerType, "controller");

            locals.Add(controller);

            // Label for return statement
            var returnTarget = Expression.Label(typeof(Task));

            // Contents of lambda body
            var bodyContents = new List <Expression>();

            // Build lambda arguments
            var parameters       = method.GetParameters();
            var parameterCount   = parameters.Length;
            var handlerArguments = new List <Expression>();

            for (var i = 0; i < parameterCount; i++)
            {
                var parameter     = parameters[i];
                var parameterType = parameter.ParameterType;
                var failedToUseRequestDataAttributes = false;

                // First, check for generic request data interfaces in attributes
                var requestDataInterfaces = parameter.GetCustomAttributes <Attribute>()
                                            .Aggregate(new List <(Attribute Attr, Type Intf)>(), (list, attr) => {
                    list.AddRange(attr.GetType().GetInterfaces()
                                  .Where(x => x.IsConstructedGenericType &&
                                         x.GetGenericTypeDefinition() == typeof(IRequestDataAttribute <,>))
                                  .Select(x => (attr, x)));

                    return(list);
                });

                // If there are any...
                if (requestDataInterfaces.Count > 0)
                {
                    // Take the first that applies to both controller and parameter type
                    var(attr, intf) = requestDataInterfaces.FirstOrDefault(
                        x => x.Intf.GenericTypeArguments[0].IsAssignableFrom(controllerType) &&
                        parameterType.IsAssignableFrom(x.Intf.GenericTypeArguments[1]));

                    if (attr != null)
                    {
                        // Use the request data interface to get a value for the parameter.
                        Expression useRequestDataInterface = Expression.Call(
                            Expression.Constant(attr),
                            intf.GetMethod(GetRequestDataAsyncMethodName),
                            controller,
                            Expression.Constant(parameter.Name));

                        // We should await the call to GetRequestDataAsync.
                        // For lack of a better way, call AwaitResult with an appropriate type argument.
                        useRequestDataInterface = Expression.Call(
                            AwaitResultMethod.MakeGenericMethod(intf.GenericTypeArguments[1]),
                            useRequestDataInterface);

                        handlerArguments.Add(useRequestDataInterface);
                        continue;
                    }

                    // If there is no interface to use, the user expects data to be injected
                    // but provided no way of injecting the right data type.
                    failedToUseRequestDataAttributes = true;
                }

                // Check for non-generic request data interfaces in attributes
                requestDataInterfaces = parameter.GetCustomAttributes <Attribute>()
                                        .Aggregate(new List <(Attribute Attr, Type Intf)>(), (list, attr) => {
                    list.AddRange(attr.GetType().GetInterfaces()
                                  .Where(x => x.IsConstructedGenericType &&
                                         x.GetGenericTypeDefinition() == typeof(IRequestDataAttribute <>))
                                  .Select(x => (attr, x)));

                    return(list);
                });

                // If there are any...
                if (requestDataInterfaces.Count > 0)
                {
                    // Take the first that applies to the controller
                    var(attr, intf) = requestDataInterfaces.FirstOrDefault(
                        x => x.Intf.GenericTypeArguments[0].IsAssignableFrom(controllerType));

                    if (attr != null)
                    {
                        // Use the request data interface to get a value for the parameter.
                        Expression useRequestDataInterface = Expression.Call(
                            Expression.Constant(attr),
                            intf.GetMethod(GetRequestDataAsyncMethodName),
                            controller,
                            Expression.Constant(parameterType),
                            Expression.Constant(parameter.Name));

                        // We should await the call to GetRequestDataAsync,
                        // then cast the result to the parameter type.
                        // For lack of a better way to do the former,
                        // and to save one function call,
                        // just call AwaitAndCastResult with an appropriate type argument.
                        useRequestDataInterface = Expression.Call(
                            AwaitAndCastResultMethod.MakeGenericMethod(parameterType),
                            Expression.Constant(parameter.Name),
                            useRequestDataInterface);

                        handlerArguments.Add(useRequestDataInterface);
                        continue;
                    }

                    // If there is no interface to use, the user expects data to be injected
                    // but provided no way of injecting the right data type.
                    failedToUseRequestDataAttributes = true;
                }

                // There are request data attributes, but none is suitable
                // for the type of the parameter.
                if (failedToUseRequestDataAttributes)
                {
                    throw new InvalidOperationException($"No request data attribute for parameter {parameter.Name} of method {controllerType.Name}.{method.Name} can provide the expected data type.");
                }

                // Check whether the name of the handler parameter matches the name of a route parameter.
                var index = IndexOfRouteParameter(matcher, parameter.Name);
                if (index >= 0)
                {
                    // Convert the parameter to the handler's parameter type.
                    var convertFromRoute = FromString.ConvertExpressionTo(
                        parameterType,
                        Expression.Property(routeInLambda, "Item", Expression.Constant(index)));

                    handlerArguments.Add(convertFromRoute);
                    continue;
                }

                // No route parameter has the same name as a handler parameter.
                // Pass the default for the parameter type.
                handlerArguments.Add(Expression.Constant(parameter.HasDefaultValue
                    ? parameter.DefaultValue
                        : parameterType.IsValueType
                        ? Activator.CreateInstance(parameterType)
                        : null));
            }

            // Create the controller and initialize its properties
            bodyContents.Add(Expression.Assign(controller, factoryExpression));
            bodyContents.Add(Expression.Call(controller, HttpContextSetter, contextInLambda));
            bodyContents.Add(Expression.Call(controller, RouteSetter, routeInLambda));

            // Build the handler method call
            Expression callMethod       = Expression.Call(controller, method, handlerArguments);
            var        methodReturnType = method.ReturnType;

            if (methodReturnType == typeof(Task))
            {
                // Nothing to do
            }
            else if (methodReturnType == typeof(void))
            {
                // Convert void to Task by evaluating Task.CompletedTask
                callMethod = Expression.Block(typeof(Task), callMethod, Expression.Constant(Task.CompletedTask));
            }
            else if (IsGenericTaskType(methodReturnType, out var resultType))
            {
                // Return a Task that serializes the result of a Task<TResult>
                callMethod = Expression.Call(
                    Expression.Constant(this),
                    SerializeResultAsyncMethod.MakeGenericMethod(resultType),
                    contextInLambda,
                    callMethod);
            }
            else
            {
                // Return a Task that serializes a result obtained synchronously
                callMethod = Expression.Call(
                    Serializer.Target == null ? null : Expression.Constant(Serializer.Target),
                    Serializer.Method,
                    contextInLambda,
                    Expression.Convert(callMethod, typeof(object)));
            }

            // Operations to perform on the controller.
            // Pseudocode:
            //     controller.PreProcessRequest();
            //     return controller.method(handlerArguments);
            Expression workWithController = Expression.Block(
                Expression.Call(controller, PreProcessRequestMethod),
                Expression.Return(returnTarget, callMethod));

            // If the controller type implements IDisposable,
            // wrap operations in a simulated using block.
            if (typeof(IDisposable).IsAssignableFrom(controllerType))
            {
                // Pseudocode:
                //     try
                //     {
                //         body();
                //     }
                //     finally
                //     {
                //         (controller as IDisposable).Dispose();
                //     }
                workWithController = Expression.TryFinally(
                    workWithController,
                    Expression.Call(Expression.TypeAs(controller, typeof(IDisposable)), DisposeMethod));
            }

            bodyContents.Add(workWithController);

            // At the end of the lambda body is the target of return statements.
            bodyContents.Add(Expression.Label(returnTarget, Expression.Constant(Task.FromResult(false))));

            // Build and compile the lambda.
            return(Expression.Lambda <RouteHandlerCallback>(
                       Expression.Block(locals, bodyContents),
                       contextInLambda,
                       routeInLambda)
                   .Compile());
        }