public static HttpModel FromType(Type type) { var model = new HttpModel(); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); var routeAttribute = type.GetCustomAttribute <RouteAttribute>(); foreach (var method in methods) { var attribute = method.GetCustomAttribute <HttpMethodAttribute>(); var httpMethod = attribute?.Method; var template = CombineRoute(routeAttribute?.Template, attribute?.Template ?? method.GetCustomAttribute <RouteAttribute>()?.Template); var methodModel = new MethodModel { MethodInfo = method, ReturnType = method.ReturnType, HttpMethod = httpMethod, }; if (template != null) { methodModel.Route(template); } foreach (var parameter in method.GetParameters()) { var fromQuery = parameter.GetCustomAttribute <FromQueryAttribute>(); var fromHeader = parameter.GetCustomAttribute <FromHeaderAttribute>(); var fromForm = parameter.GetCustomAttribute <FromFormAttribute>(); var fromBody = parameter.GetCustomAttribute <FromBodyAttribute>(); var fromRoute = parameter.GetCustomAttribute <FromRouteAttribute>(); var fromCookie = parameter.GetCustomAttribute <FromCookieAttribute>(); var fromService = parameter.GetCustomAttribute <FromServicesAttribute>(); methodModel.Parameters.Add(new ParameterModel { Name = parameter.Name, ParameterType = parameter.ParameterType, FromQuery = fromQuery == null ? null : fromQuery?.Name ?? parameter.Name, FromHeader = fromHeader == null ? null : fromHeader?.Name ?? parameter.Name, FromForm = fromForm == null ? null : fromForm?.Name ?? parameter.Name, FromRoute = fromRoute == null ? null : fromRoute?.Name ?? parameter.Name, FromCookie = fromCookie == null ? null : fromCookie?.Name, FromBody = fromBody != null, FromServices = fromService != null }); } model.Methods.Add(methodModel); } return(model); }
public static MethodModel Method(this HttpModel model, string method) { return(model.Methods.First(m => m.MethodInfo.Name == method)); }
private static List <Endpoint> BuildWithoutCache(Type handlerType, Action <HttpModel> configure) { var model = HttpModel.FromType(handlerType); configure?.Invoke(model); var endpoints = new List <Endpoint>(); foreach (var method in model.Methods.Where(m => m.RoutePattern != null)) { var needForm = false; var httpMethod = method.HttpMethod; var template = method.RoutePattern; // Non void return type // Task Invoke(HttpContext httpContext, RouteValueDictionary routeValues, RequestDelegate next) // { // // The type is activated via DI if it has args // return ExecuteResult(new THttpHandler(...).Method(..), httpContext); // } // void return type // Task Invoke(HttpContext httpContext, RouteValueDictionary routeValues, RequestDelegate next) // { // new THttpHandler(...).Method(..) // return Task.CompletedTask; // } var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); var routeValuesArg = Expression.Parameter(typeof(RouteValueDictionary), "routeValues"); var nextArg = Expression.Parameter(typeof(RequestDelegate), "next"); var requestServicesExpr = Expression.Property(httpContextArg, nameof(HttpContext.RequestServices)); // Fast path: We can skip the activator if there's only a default ctor with 0 args var ctors = handlerType.GetConstructors(); Expression httpHandlerExpression = null; if (method.MethodInfo.IsStatic) { // Do nothing } else if (ctors.Length == 1 && ctors[0].GetParameters().Length == 0) { httpHandlerExpression = Expression.New(ctors[0]); } else { // CreateInstance<THttpHandler>(context.RequestServices) httpHandlerExpression = Expression.Call(ActivatorMethodInfo.MakeGenericMethod(handlerType), requestServicesExpr); } var args = new List <Expression>(); var httpRequestExpr = Expression.Property(httpContextArg, nameof(HttpContext.Request)); foreach (var parameter in method.Parameters) { Expression paramterExpression = Expression.Default(parameter.ParameterType); if (parameter.FromQuery != null) { var queryProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Query)); paramterExpression = BindArgument(queryProperty, parameter, parameter.FromQuery); } else if (parameter.FromHeader != null) { var headersProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Headers)); paramterExpression = BindArgument(headersProperty, parameter, parameter.FromHeader); } else if (parameter.FromRoute != null) { paramterExpression = BindArgument(routeValuesArg, parameter, parameter.FromRoute); } else if (parameter.FromCookie != null) { var cookiesProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Cookies)); paramterExpression = BindArgument(cookiesProperty, parameter, parameter.FromCookie); } else if (parameter.FromServices) { paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), requestServicesExpr); } else if (parameter.FromForm != null) { needForm = true; var formProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Form)); paramterExpression = BindArgument(formProperty, parameter, parameter.FromForm); } else if (parameter.FromBody) { var bodyProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Body)); paramterExpression = BindBody(bodyProperty, parameter); } else { if (parameter.ParameterType == typeof(IFormCollection)) { paramterExpression = Expression.Property(httpRequestExpr, nameof(HttpRequest.Form)); } else if (parameter.ParameterType == typeof(HttpContext)) { paramterExpression = httpContextArg; } else if (parameter.ParameterType == typeof(RequestDelegate)) { paramterExpression = nextArg; } else if (parameter.ParameterType == typeof(IHeaderDictionary)) { paramterExpression = Expression.Property(httpRequestExpr, nameof(HttpRequest.Headers)); } } args.Add(paramterExpression); } Expression body = null; if (method.ReturnType == typeof(void)) { var bodyExpressions = new List <Expression> { Expression.Call(httpHandlerExpression, method.MethodInfo, args), Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo) }; body = Expression.Block(bodyExpressions); } else { var methodCall = Expression.Call(httpHandlerExpression, method.MethodInfo, args); // Coerce Task<T> to Task<object> if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task <>)) { var typeArg = method.ReturnType.GetGenericArguments()[0]; // ExecuteTask<T>(handler.Method(..), httpContext); body = Expression.Call( ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg), methodCall, httpContextArg); } else { // ExecuteResult(handler.Method(..), httpContext); body = Expression.Call(ExecuteAsyncMethodInfo, methodCall, httpContextArg); } } var lambda = Expression.Lambda <Func <HttpContext, RouteValueDictionary, RequestDelegate, Task> >(body, httpContextArg, routeValuesArg, nextArg); var routeTemplate = method.RoutePattern; var invoker = lambda.Compile(); var routeEndpointModel = new RouteEndpointModel( async httpContext => { // Generating async code would just be insane so if the method needs the form populate it here // so the within the method it's cached if (needForm) { await httpContext.Request.ReadFormAsync(); } await invoker.Invoke(httpContext, httpContext.Request.RouteValues, (c) => Task.CompletedTask); }, routeTemplate, 0); routeEndpointModel.DisplayName = routeTemplate.RawText; if (!string.IsNullOrEmpty(method.HttpMethod)) { routeEndpointModel.Metadata.Add(new HttpMethodMetadata(new[] { method.HttpMethod })); } foreach (var attribute in method.MethodInfo.GetCustomAttributes(true)) { routeEndpointModel.Metadata.Add(attribute); } foreach (var convention in method.Conventions) { convention(routeEndpointModel); } endpoints.Add(routeEndpointModel.Build()); } return(endpoints); }
// Expression tree impl internal static void Build(Type handlerType, IEndpointRouteBuilder routes) { var model = HttpModel.FromType(handlerType); ObjectFactory factory = null; // REVIEW: Should this be lazy? var httpRequestReader = routes.ServiceProvider.GetRequiredService <IHttpRequestReader>(); foreach (var method in model.Methods) { // Nothing to route to if (method.RoutePattern == null) { continue; } var needForm = false; var needBody = false; Type bodyType = null; // Non void return type // Task Invoke(HttpContext httpContext) // { // // The type is activated via DI if it has args // return ExecuteResultAsync(new THttpHandler(...).Method(..), httpContext); // } // void return type // Task Invoke(HttpContext httpContext) // { // new THttpHandler(...).Method(..) // return Task.CompletedTask; // } var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); // This argument represents the deserialized body returned from IHttpRequestReader // when the method has a FromBody attribute declared var deserializedBodyArg = Expression.Parameter(typeof(object), "bodyValue"); var requestServicesExpr = Expression.Property(httpContextArg, nameof(HttpContext.RequestServices)); // Fast path: We can skip the activator if there's only a default ctor with 0 args var ctors = handlerType.GetConstructors(); Expression httpHandlerExpression = null; if (method.MethodInfo.IsStatic) { // Do nothing } else if (ctors.Length == 1 && ctors[0].GetParameters().Length == 0) { httpHandlerExpression = Expression.New(ctors[0]); } else { // Create a factory lazily for this handlerType if (factory == null) { factory = ActivatorUtilities.CreateFactory(handlerType, Type.EmptyTypes); } // This invokes the cached factory to create the instance then casts it to the target type var invokeFactoryExpr = Expression.Invoke(Expression.Constant(factory), requestServicesExpr, Expression.Constant(null, typeof(object[]))); httpHandlerExpression = Expression.Convert(invokeFactoryExpr, handlerType); } var args = new List <Expression>(); var httpRequestExpr = Expression.Property(httpContextArg, nameof(HttpContext.Request)); foreach (var parameter in method.Parameters) { Expression paramterExpression = Expression.Default(parameter.ParameterType); if (parameter.FromQuery != null) { var queryProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Query)); paramterExpression = BindArgument(queryProperty, parameter, parameter.FromQuery); } else if (parameter.FromHeader != null) { var headersProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Headers)); paramterExpression = BindArgument(headersProperty, parameter, parameter.FromHeader); } else if (parameter.FromRoute != null) { var routeValuesProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.RouteValues)); paramterExpression = BindArgument(routeValuesProperty, parameter, parameter.FromRoute); } else if (parameter.FromCookie != null) { var cookiesProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Cookies)); paramterExpression = BindArgument(cookiesProperty, parameter, parameter.FromCookie); } else if (parameter.FromServices) { paramterExpression = Expression.Call(GetRequiredServiceMethodInfo.MakeGenericMethod(parameter.ParameterType), requestServicesExpr); } else if (parameter.FromForm != null) { needForm = true; var formProperty = Expression.Property(httpRequestExpr, nameof(HttpRequest.Form)); paramterExpression = BindArgument(formProperty, parameter, parameter.FromForm); } else if (parameter.FromBody) { if (needBody) { throw new InvalidOperationException(method.MethodInfo.Name + " cannot have more than one FromBody attribute."); } if (needForm) { throw new InvalidOperationException(method.MethodInfo.Name + " cannot mix FromBody and FromForm on the same method."); } needBody = true; bodyType = parameter.ParameterType; paramterExpression = Expression.Convert(deserializedBodyArg, bodyType); } else { if (parameter.ParameterType == typeof(IFormCollection)) { needForm = true; paramterExpression = Expression.Property(httpRequestExpr, nameof(HttpRequest.Form)); } else if (parameter.ParameterType == typeof(HttpContext)) { paramterExpression = httpContextArg; } } args.Add(paramterExpression); } Expression body = null; var methodCall = Expression.Call(httpHandlerExpression, method.MethodInfo, args); // Exact request delegate match if (method.MethodInfo.ReturnType == typeof(void)) { var bodyExpressions = new List <Expression> { methodCall, Expression.Property(null, (PropertyInfo)CompletedTaskMemberInfo) }; body = Expression.Block(bodyExpressions); } else if (AwaitableInfo.IsTypeAwaitable(method.MethodInfo.ReturnType, out var info)) { if (method.MethodInfo.ReturnType == typeof(Task)) { body = methodCall; } else if (method.MethodInfo.ReturnType.IsGenericType && method.MethodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task <>)) { var typeArg = method.MethodInfo.ReturnType.GetGenericArguments()[0]; if (typeof(Result).IsAssignableFrom(typeArg)) { body = Expression.Call( ExecuteTaskResultOfTMethodInfo.MakeGenericMethod(typeArg), methodCall, httpContextArg); } else { // ExecuteTask<T>(handler.Method(..), httpContext); body = Expression.Call( ExecuteTaskOfTMethodInfo.MakeGenericMethod(typeArg), methodCall, httpContextArg); } } else if (method.MethodInfo.ReturnType.IsGenericType && method.MethodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask <>)) { var typeArg = method.MethodInfo.ReturnType.GetGenericArguments()[0]; if (typeof(Result).IsAssignableFrom(typeArg)) { body = Expression.Call( ExecuteValueResultTaskOfTMethodInfo.MakeGenericMethod(typeArg), methodCall, httpContextArg); } else { // ExecuteTask<T>(handler.Method(..), httpContext); body = Expression.Call( ExecuteValueTaskOfTMethodInfo.MakeGenericMethod(typeArg), methodCall, httpContextArg); } } else { // TODO: Handle custom awaitables throw new NotSupportedException("Unsupported return type " + method.MethodInfo.ReturnType); } } else if (typeof(Result).IsAssignableFrom(method.MethodInfo.ReturnType)) { body = Expression.Call(methodCall, ResultExecuteAsync, httpContextArg); } else { var newObjectResult = Expression.New(ObjectResultCtor, methodCall); body = Expression.Call(newObjectResult, ObjectResultExecuteAsync, httpContextArg); } RequestDelegate requestDelegate = null; if (needBody) { // We need to generate the code for reading from the body before calling into the // delegate var lambda = Expression.Lambda <Func <HttpContext, object, Task> >(body, httpContextArg, deserializedBodyArg); var invoker = lambda.Compile(); requestDelegate = async httpContext => { var bodyValue = await httpRequestReader.ReadAsync(httpContext, bodyType); await invoker(httpContext, bodyValue); }; } else if (needForm) { var lambda = Expression.Lambda <RequestDelegate>(body, httpContextArg); var invoker = lambda.Compile(); requestDelegate = async httpContext => { // Generating async code would just be insane so if the method needs the form populate it here // so the within the method it's cached await httpContext.Request.ReadFormAsync(); await invoker(httpContext); }; } else { var lambda = Expression.Lambda <RequestDelegate>(body, httpContextArg); var invoker = lambda.Compile(); requestDelegate = invoker; } var displayName = method.MethodInfo.DeclaringType.Name + "." + method.MethodInfo.Name; routes.Map(method.RoutePattern, requestDelegate).Add(b => { foreach (var item in method.Metadata) { b.Metadata.Add(item); } }); } }