[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 _)); }
[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 _)); }
public CustomModule(string baseRoute) { BaseRoute = Validate.Route(nameof(baseRoute), baseRoute, true); _routeMatcher = RouteMatcher.Parse(baseRoute, true); LogSource = GetType().Name; }
/// <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; }
// 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()); }