using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using EmbedIO.Routing; using EmbedIO.Utilities; using Swan; namespace EmbedIO.WebApi { /// /// A module using objects derived from /// as collections of handler methods. /// public abstract class WebApiModuleBase : RoutingModuleBase { private const string GetRequestDataAsyncMethodName = nameof(IRequestDataAttribute.GetRequestDataAsync); private static readonly MethodInfo PreProcessRequestMethod = typeof(WebApiController).GetMethod(nameof(WebApiController.PreProcessRequest)); private static readonly MethodInfo HttpContextSetter = typeof(WebApiController).GetProperty(nameof(WebApiController.HttpContext)).GetSetMethod(true); private static readonly MethodInfo RouteSetter = typeof(WebApiController).GetProperty(nameof(WebApiController.Route)).GetSetMethod(true); private static readonly MethodInfo AwaitResultMethod = typeof(WebApiModuleBase).GetMethod(nameof(AwaitResult), BindingFlags.Static | BindingFlags.NonPublic); private static readonly MethodInfo AwaitAndCastResultMethod = typeof(WebApiModuleBase).GetMethod(nameof(AwaitAndCastResult), BindingFlags.Static | BindingFlags.NonPublic); private static readonly MethodInfo DisposeMethod = typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose)); private static readonly MethodInfo SerializeResultAsyncMethod = typeof(WebApiModuleBase).GetMethod(nameof(SerializeResultAsync), BindingFlags.Instance | BindingFlags.NonPublic); private readonly HashSet _controllerTypes = new HashSet(); /// /// Initializes a new instance of the class, /// using the default response serializer. /// /// The base route served by this module. /// /// protected WebApiModuleBase(string baseRoute) : this(baseRoute, ResponseSerializer.Default) { } /// /// Initializes a new instance of the class, /// using the specified response serializer. /// /// The base route served by this module. /// A used to serialize /// the result of controller methods returning /// or Task<object>. /// is . /// /// protected WebApiModuleBase(string baseRoute, ResponseSerializerCallback serializer) : base(baseRoute) { Serializer = Validate.NotNull(nameof(serializer), serializer); } /// /// A used to serialize /// the result of controller methods returning values. /// public ResponseSerializerCallback Serializer { get; } /// /// Gets the number of controller types registered in this module. /// public int ControllerCount => _controllerTypes.Count; /// /// Registers a controller type using a constructor. /// In order for registration to be successful, the specified controller type: /// /// must be a subclass of ; /// must not be an abstract class; /// must not be a generic type definition; /// must have a public parameterless constructor. /// /// /// The type of the controller. /// The module's configuration is locked. /// /// is already registered in this module. /// does not satisfy the prerequisites /// listed in the Summary section. /// /// /// A new instance of will be created /// for each request to handle, and dereferenced immediately afterwards, /// to be collected during next garbage collection cycle. /// is not required to be thread-safe, /// as it will be constructed and used in the same synchronization context. /// However, since request handling is asynchronous, the actual execution thread /// may vary during execution. Care must be exercised when using thread-sensitive /// resources or thread-static data. /// If implements , /// its Dispose method will be called when it has /// finished handling a request. /// /// /// protected void RegisterControllerType() where TController : WebApiController, new() => RegisterControllerType(typeof(TController)); /// /// Registers a controller type using a factory method. /// In order for registration to be successful: /// /// must be a subclass of ; /// must not be a generic type definition; /// 's return type must be either /// or a subclass of . /// /// /// The type of the controller. /// The factory method used to construct instances of . /// The module's configuration is locked. /// is . /// /// is already registered in this module. /// - or - /// does not satisfy the prerequisites listed in the Summary section. /// /// /// will be called once for each request to handle /// in order to obtain an instance of . /// The returned instance will be dereferenced immediately after handling the request. /// is not required to be thread-safe, /// as it will be constructed and used in the same synchronization context. /// However, since request handling is asynchronous, the actual execution thread /// may vary during execution. Care must be exercised when using thread-sensitive /// resources or thread-static data. /// If implements , /// its Dispose method will be called when it has /// finished handling a request. In this case it is recommended that /// return a newly-constructed instance of /// at each invocation. /// If does not implement , /// may employ techniques such as instance pooling to avoid /// the overhead of constructing a new instance of /// at each invocation. If so, resources such as file handles, database connections, etc. /// should be freed before returning from each handler method to avoid /// starvation. /// /// /// protected void RegisterControllerType(Func factory) where TController : WebApiController => RegisterControllerType(typeof(TController), factory); /// /// Registers a controller type using a constructor. /// In order for registration to be successful, the specified : /// /// must be a subclass of ; /// must not be an abstract class; /// must not be a generic type definition; /// must have a public parameterless constructor. /// /// /// The type of the controller. /// The module's configuration is locked. /// is . /// /// is already registered in this module. /// - or - /// does not satisfy the prerequisites /// listed in the Summary section. /// /// /// A new instance of will be created /// for each request to handle, and dereferenced immediately afterwards, /// to be collected during next garbage collection cycle. /// is not required to be thread-safe, /// as it will be constructed and used in the same synchronization context. /// However, since request handling is asynchronous, the actual execution thread /// may vary during execution. Care must be exercised when using thread-sensitive /// resources or thread-static data. /// If implements , /// its Dispose method will be called when it has /// finished handling a request. /// /// /// protected void RegisterControllerType(Type controllerType) { EnsureConfigurationNotLocked(); controllerType = ValidateControllerType(nameof(controllerType), controllerType, false); var constructor = controllerType.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0); if (constructor == null) { throw new ArgumentException( "Controller type must have a public parameterless constructor.", nameof(controllerType)); } if (!TryRegisterControllerTypeCore(controllerType, Expression.New(constructor))) throw new ArgumentException($"Type {controllerType.Name} contains no controller methods."); } /// /// Registers a controller type using a factory method. /// In order for registration to be successful: /// /// must be a subclass of ; /// must not be a generic type definition; /// 's return type must be either /// or a subclass of . /// /// /// The type of the controller. /// The factory method used to construct instances of . /// The module's configuration is locked. /// /// is . /// - or - /// is . /// /// /// is already registered in this module. /// - or - /// One or more parameters do not satisfy the prerequisites listed in the Summary section. /// /// /// will be called once for each request to handle /// in order to obtain an instance of . /// The returned instance will be dereferenced immediately after handling the request. /// is not required to be thread-safe, /// as it will be constructed and used in the same synchronization context. /// However, since request handling is asynchronous, the actual execution thread /// may vary during execution. Care must be exercised when using thread-sensitive /// resources or thread-static data. /// If implements , /// its Dispose method will be called when it has /// finished handling a request. In this case it is recommended that /// return a newly-constructed instance of /// at each invocation. /// If does not implement , /// may employ techniques such as instance pooling to avoid /// the overhead of constructing a new instance of /// at each invocation. If so, resources such as file handles, database connections, etc. /// should be freed before returning from each handler method to avoid /// starvation. /// /// /// protected void RegisterControllerType(Type controllerType, Func factory) { EnsureConfigurationNotLocked(); controllerType = ValidateControllerType(nameof(controllerType), controllerType, true); factory = Validate.NotNull(nameof(factory), factory); if (!controllerType.IsAssignableFrom(factory.Method.ReturnType)) throw new ArgumentException("Factory method has an incorrect return type.", nameof(factory)); var expression = Expression.Call( factory.Target == null ? null : Expression.Constant(factory.Target), factory.Method); if (!TryRegisterControllerTypeCore(controllerType, expression)) throw new ArgumentException($"Type {controllerType.Name} contains no controller methods."); } private static int IndexOfRouteParameter(RouteMatcher matcher, string name) { var names = matcher.ParameterNames; for (var i = 0; i < names.Count; i++) { if (names[i] == name) return i; } return -1; } private static T AwaitResult(Task task) => task.ConfigureAwait(false).GetAwaiter().GetResult(); private static T AwaitAndCastResult(string parameterName, Task task) { var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); return result switch { null when typeof(T).IsValueType && Nullable.GetUnderlyingType(typeof(T)) == null => throw new InvalidCastException($"Cannot cast null to {typeof(T).FullName} for parameter \"{parameterName}\"."), null => default, T castResult => castResult, _ => throw new InvalidCastException($"Cannot cast {result.GetType().FullName} to {typeof(T).FullName} for parameter \"{parameterName}\".") }; } private static bool IsGenericTaskType(Type type, out Type? resultType) { resultType = null; if (!type.IsConstructedGenericType) return false; if (type.GetGenericTypeDefinition() != typeof(Task<>)) return false; resultType = type.GetGenericArguments()[0]; return true; } // 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, 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, RouteMatcher matcher) { // Lambda parameters var contextInLambda = Expression.Parameter(typeof(IHttpContext), "context"); var routeInLambda = Expression.Parameter(typeof(RouteMatch), "route"); // Local variables var locals = new List(); // 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(); // Build lambda arguments var parameters = method.GetParameters(); var parameterCount = parameters.Length; var handlerArguments = new List(); 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() .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() .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 ?? throw SelfCheck.Failure($"{nameof(convertFromRoute)} is null.")); continue; } // No route parameter has the same name as a handler parameter. // Pass the default for the parameter type. handlerArguments.Add(parameter.HasDefaultValue ? (Expression)Expression.Constant(parameter.DefaultValue) : Expression.Default(parameterType)); } // 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 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( Expression.Block(locals, bodyContents), contextInLambda, routeInLambda) .Compile(); } private async Task SerializeResultAsync(IHttpContext context, Task task) { await Serializer( context, await task.ConfigureAwait(false)).ConfigureAwait(false); } private Type ValidateControllerType(string argumentName, Type value, bool canBeAbstract) { value = Validate.NotNull(argumentName, value); if (canBeAbstract) { if (value.IsGenericTypeDefinition || !value.IsSubclassOf(typeof(WebApiController))) throw new ArgumentException($"Controller type must be a subclass of {nameof(WebApiController)}.", argumentName); } else { if (value.IsAbstract || value.IsGenericTypeDefinition || !value.IsSubclassOf(typeof(WebApiController))) throw new ArgumentException($"Controller type must be a non-abstract subclass of {nameof(WebApiController)}.", argumentName); } if (_controllerTypes.Contains(value)) throw new ArgumentException("Controller type is already registered in this module.", argumentName); return value; } private bool TryRegisterControllerTypeCore(Type controllerType, Expression factoryExpression) { var handlerCount = 0; var methods = controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public) .Where(m => !m.ContainsGenericParameters); foreach (var method in methods) { var attributes = method.GetCustomAttributes() .OfType() .ToArray(); if (attributes.Length < 1) continue; foreach (var attribute in attributes) { AddHandler(attribute.Verb, attribute.Matcher, CompileHandler(factoryExpression, method, attribute.Matcher)); handlerCount++; } } if (handlerCount < 1) return false; _controllerTypes.Add(controllerType); return true; } } }