123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- #if !WASM
- using System;
- using Microsoft.AspNetCore.Http;
- using System.Reflection;
- using System.Linq;
- using System.Threading.Tasks;
- using System.Collections.Generic;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.Routing;
- using System.Globalization;
- using System.Diagnostics;
- using Microsoft.AspNetCore.Routing.Template;
- using Microsoft.AspNetCore.Routing;
- using System.Threading;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- using System.IO;
- using System.Text.Json;
- namespace FastReport.Web.Infrastructure
- {
- internal static partial class ControllerBuilder
- {
- private static ControllerExecutor _executor;
- private static readonly JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true };
- public static MethodInfo[] GetControllerMethods()
- {
- return GetControllerMethods(typeof(Controllers.Controllers));
- }
- internal static MethodInfo[] GetControllerMethods(Type fromType)
- {
- return fromType.GetMethods(BindingFlags.Public | BindingFlags.Static);
- }
- public static Delegate BuildMinimalAPIDelegate(MethodInfo method)
- {
- var methodParams = method.GetParameters().Select(param => param.ParameterType).ToList();
- var returnType = method.ReturnType;
- Type delegateType;
- switch (methodParams.Count + 1) // plus returnType
- {
- case 1:
- if (returnType == typeof(void))
- delegateType = typeof(Action);
- else
- delegateType = typeof(Func<>);
- break;
- case 2:
- delegateType = typeof(Func<,>);
- break;
- case 3:
- delegateType = typeof(Func<,,>);
- break;
- case 4:
- delegateType = typeof(Func<,,,>);
- break;
- case 5:
- delegateType = typeof(Func<,,,,>);
- break;
- case 6:
- delegateType = typeof(Func<,,,,,>);
- break;
- case 7:
- delegateType = typeof(Func<,,,,,,>);
- break;
- case 8:
- delegateType = typeof(Func<,,,,,,,>);
- break;
- case 9:
- delegateType = typeof(Func<,,,,,,,,>);
- break;
- case 10:
- delegateType = typeof(Func<,,,,,,,,,>);
- break;
- default:
- throw new NotImplementedException();
- }
- if (methodParams.Count > 0)
- {
- methodParams.Add(returnType);
- delegateType = delegateType.MakeGenericType(methodParams.ToArray());
- }
- return Delegate.CreateDelegate(delegateType, method);
- }
- public static ControllerExecutor Executor => _executor;
- public static void InitializeControllers()
- {
- var methods = GetControllerMethods();
- InitializeControllers(methods);
- }
- internal static void InitializeControllers(MethodInfo[] methods)
- {
- var endpoints = BuildEndpoints(methods);
- _executor = new ControllerExecutor(endpoints);
- }
- internal static EndpointInfo[] BuildEndpoints(IReadOnlyCollection<MethodInfo> methods)
- {
- //var availableServices = GetFastReportServices();
- List<EndpointInfo> endpoints = new List<EndpointInfo>();
- foreach (var method in methods)
- {
- var endpoint = BuildEndpoint(method);
- endpoints.AddRange(endpoint);
- }
- return endpoints.ToArray();
- }
- internal static IEnumerable<EndpointInfo> BuildEndpoint(MethodInfo method)
- {
- var httpMethodAttribute = method.GetCustomAttribute<HttpMethodAttribute>();
- string routeTemplate = httpMethodAttribute?.Template ?? method.GetCustomAttribute<RouteAttribute>()?.Template;
- if (routeTemplate == null)
- throw new Exception($"Method '{method.Name}' doesn't have a route template. Please, use RouteAttribute or Http[Get|Post|...]Attribute with route template");
- var parameters = new List<Func<HttpContext, object>>();
- var parameterInfos = method.GetParameters();
- for (int i = 0; i < parameterInfos.Length; i++)
- {
- var parameterInfo = parameterInfos[i];
- var paramType = parameterInfo.ParameterType;
- // if FromQuery/FromRoute/FromBody etc.
- if (TryAttributesSearching(parameters, parameterInfo, paramType))
- continue;
- // if knownTypes:
- if (TryKnownTypesSearching(parameters, parameterInfo.Name, paramType))
- continue;
- // if exist in serviceProvider
- parameters.Add((httpContext) => httpContext.RequestServices.GetService(paramType));
- continue;
- }
- var resultHandler = BuildResultHandler(method.ReturnType);
- var templateMatcher = new TemplateMatcher(TemplateParser.Parse(WebUtils.ToUrl(FastReportGlobal.FastReportOptions.RouteBasePath, routeTemplate).TrimStart('/')), new RouteValueDictionary());
- if(httpMethodAttribute != null)
- foreach(var httpMethod in httpMethodAttribute.HttpMethods)
- {
- yield return new EndpointInfo(httpMethod,
- templateMatcher,
- method,
- parameters.ToArray(),
- resultHandler);
- }
- else
- {
- // if HttpMethodAttribute doesn't exists => it's GET
- yield return new EndpointInfo("GET",
- templateMatcher,
- method,
- parameters.ToArray(),
- resultHandler);
- }
- }
- private static Func<HttpContext, object, Task> BuildResultHandler(Type returnType)
- {
- PropertyInfo resultProperty;
- if(returnType.IsGenericType)
- {
- var generticType = returnType.GetGenericTypeDefinition();
- if (generticType == typeof(Task<>))
- {
- resultProperty = returnType.GetProperty(nameof(Task<object>.Result));
- return async (httpContext, methodResult) =>
- {
- await (Task)methodResult;
- var result = resultProperty.GetValue(methodResult);
- await BuilderTemplates.HandleResult(httpContext, result);
- };
- }
- else if (generticType == typeof(ValueTask<>))
- {
- resultProperty = returnType.GetProperty(nameof(ValueTask<object>.Result));
- return async (httpContext, methodResult) =>
- {
- // TODO: NOT WORKING
- await (ValueTask<object>)methodResult;
- var result = resultProperty.GetValue(methodResult);
- await BuilderTemplates.HandleResult(httpContext, result);
- };
- }
- }
- else if (returnType == typeof(Task))
- {
- return BuilderTemplates.Task_AwaitAndHandleResult;
- }
- else if (returnType == typeof(ValueTask))
- {
- return BuilderTemplates.ValueTask_AwaitAndHandleResult;
- }
- return BuilderTemplates.HandleResult;
- }
- internal static class BuilderTemplates
- {
- private static readonly IResult _SuccessResult = Results.Ok();
- public static async Task Task_AwaitAndHandleResult(HttpContext httpContext, object methodResult)
- {
- await(Task)methodResult;
- await HandleResult(httpContext, null);
- }
- public static async Task ValueTask_AwaitAndHandleResult(HttpContext httpContext, object methodResult)
- {
- await (ValueTask)methodResult;
- await HandleResult(httpContext, null);
- }
- public static Task HandleResult(HttpContext httpContext, object result)
- {
- if (result == null)
- {
- return _SuccessResult.ExecuteAsync(httpContext);
- }
- if (result is IResult iResult)
- {
- return iResult.ExecuteAsync(httpContext);
- }
- else
- {
- Debug.Assert(result.GetType() != typeof(IActionResult));
- string content = System.Text.Json.JsonSerializer.Serialize(result);
- var contentResult = Results.Content(content, "text/json");
- return contentResult.ExecuteAsync(httpContext);
- }
- }
- }
- private static bool TryKnownTypesSearching(List<Func<HttpContext, object>> parameters, string paramName, Type paramType)
- {
- if (paramType == typeof(HttpContext))
- {
- parameters.Add(static (httpContext) => httpContext);
- return true;
- }
- else if (paramType == typeof(HttpRequest))
- {
- parameters.Add(static (httpContext) => httpContext.Request);
- return true;
- }
- else if (paramType == typeof(HttpResponse))
- {
- parameters.Add(static (httpContext) => httpContext.Response);
- return true;
- }
- else if (paramType == typeof(CancellationToken))
- {
- parameters.Add(static (httpContext) => httpContext.RequestAborted);
- return true;
- }
- else if (paramType == typeof(IServiceProvider))
- {
- parameters.Add(static (httpContext) => httpContext.RequestServices);
- return true;
- }
- // Special case: we check primitive types
- else if (paramType.IsSimpleType())
- {
- parameters.Add((httpContext) =>
- {
- var request = httpContext.Request;
- if (request.Query.TryGetValue(paramName, out var queryValue))
- return ParseToType(queryValue, paramType);
- if (request.Headers.TryGetValue(paramName, out var headerValue))
- return ParseToType(headerValue, paramType);
- if (request.RouteValues.TryGetValue(paramName, out var routeValue))
- return ParseToType(routeValue, paramType);
- if (request.Form.TryGetValue(paramName, out var formValue))
- return ParseToType(formValue, paramType);
- return default;
- });
- return true;
- }
- return false;
- }
- private static bool TryAttributesSearching(List<Func<HttpContext, object>> parameters, ParameterInfo parameterInfo, Type paramType)
- {
- var attributes = parameterInfo.GetCustomAttributes(false);
- foreach (var attribute in attributes)
- {
- Func<HttpContext, object> func;
- if (attribute is FromQueryAttribute fromQuery)
- {
- string name = fromQuery.Name ?? parameterInfo.Name;
- if (paramType.IsSimpleType())
- func = (httpContext) =>
- {
- var queryValue = httpContext.Request.Query[name].ToString();
- return ParseToType(queryValue, paramType);
- };
- else
- func = (httpContext) =>
- {
- return DeserializeFromCollection(httpContext.Request.Query, paramType);
- };
- parameters.Add(func);
- return true;
- }
- else if (attribute is FromBodyAttribute)
- {
- func = (httpContext) =>
- {
- using var reader = new StreamReader(httpContext.Request.Body);
- var requestBody = reader.ReadToEndAsync().GetAwaiter().GetResult();
- return JsonSerializer.Deserialize(requestBody, paramType, _serializerOptions);
- };
- parameters.Add(func);
- return true;
- }
- else if (attribute is FromHeaderAttribute fromHeader)
- {
- string name = fromHeader.Name ?? parameterInfo.Name;
- if (paramType.IsSimpleType())
- func = (httpContext) =>
- {
- var queryValue = httpContext.Request.Headers[name].ToString();
- return ParseToType(queryValue, paramType);
- };
- else
- func = (httpContext) =>
- {
- return DeserializeFromCollection(httpContext.Request.Headers, paramType);
- };
- parameters.Add(func);
- return true;
- }
- else if (attribute is FromRouteAttribute fromRoute)
- {
- string name = fromRoute.Name ?? parameterInfo.Name;
- if (paramType.IsSimpleType())
- func = (httpContext) =>
- {
- var queryValue = httpContext.Request.RouteValues[name].ToString();
- return ParseToType(queryValue, paramType);
- };
- else
- func = (httpContext) =>
- {
- return DeserializeFromCollection(httpContext.Request.RouteValues, paramType);
- };
- parameters.Add(func);
- return true;
- }
- else if (attribute is FromServicesAttribute)
- {
- parameters.Add((httpContext) =>
- {
- return httpContext.RequestServices.GetService(paramType);
- });
- return true;
- }
- }
- return false;
- }
- private static object DeserializeFromCollection(IDictionary<string, object> dictionary, Type paramType)
- {
- var result = Activator.CreateInstance(paramType);
- var properties = paramType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
- foreach (var property in properties)
- {
- if (dictionary.TryGetValue(property.Name, out var value))
- {
- var parsedValue = ParseToType(value.ToString(), property.PropertyType);
- property.SetValue(result, parsedValue);
- }
- }
- return result;
- }
- private static object DeserializeFromCollection(IQueryCollection dictionary, Type paramType)
- {
- var result = Activator.CreateInstance(paramType);
- // TODO: test with init-only fields
- var properties = paramType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
- foreach (var property in properties)
- {
- if (dictionary.TryGetValue(property.Name, out var value))
- {
- var parsedValue = ParseToType(value.ToString(), property.PropertyType);
- property.SetValue(result, parsedValue);
- }
- }
- return result;
- }
- private static object DeserializeFromCollection(IHeaderDictionary dictionary, Type paramType)
- {
- var result = Activator.CreateInstance(paramType);
- var properties = paramType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
- foreach (var property in properties)
- {
- if (dictionary.TryGetValue(property.Name, out var value))
- {
- var parsedValue = ParseToType(value.ToString(), property.PropertyType);
- property.SetValue(result, parsedValue);
- }
- }
- return result;
- }
- private static object ParseToType(string value, Type type)
- {
- object result;
- if (type.IsPrimitive || type == typeof(string))
- {
- result = Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
- }
- else // if (type.IsEnum)
- {
- result = Enum.Parse(type, value);
- }
- return result;
- }
- private static object ParseToType(object value, Type type)
- {
- object result;
- if (type.IsPrimitive || type == typeof(string))
- {
- result = Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
- }
- else // if (type.IsEnum)
- {
- result = Enum.Parse(type, value.ToString());
- }
- return result;
- }
- [DebuggerDisplay("{Method.Name} [{Parameters.Length}], {HttpMethod} {Route.Template}")]
- internal readonly struct EndpointInfo
- {
- public readonly string HttpMethod;
- public readonly TemplateMatcher Route;
- public readonly Func<HttpContext, object>[] Parameters;
- public readonly MethodInfo Method;
- public readonly Func<HttpContext, object, Task> Handler;
- public EndpointInfo(string httpMethod, TemplateMatcher route, MethodInfo method,
- Func<HttpContext, object>[] parameters,
- Func<HttpContext, object, Task> handler)
- {
- HttpMethod = httpMethod;
- Route = route;
- Method = method;
- Parameters = parameters;
- Handler = handler;
- }
- }
- [DebuggerDisplay("{Method.Name} [{Parameters.Length}], {HttpMethod} {Route.Template}")]
- internal readonly struct BuiltEndpointInfo
- {
- public readonly string HttpMethod;
- public readonly TemplateMatcher Route;
- public readonly Func<HttpContext, Task> Method;
- public BuiltEndpointInfo(string httpMethod, TemplateMatcher route, Func<HttpContext, Task> method)
- {
- HttpMethod = httpMethod;
- Route = route;
- Method = method;
- }
- }
- //private static Type[] GetFastReportServices()
- //{
- // var types = typeof(ControllerBuilder).Assembly.GetTypes();
- // var services = new List<Type>();
- // foreach (var type in types)
- // {
- // if(type.IsInterface && type.IsPublic)
- // {
- // services.Add(type);
- // }
- // }
- // return services.ToArray();
- //}
- }
- internal static class TypeExtensions
- {
- public static bool IsSimpleType(this Type type)
- => type.IsPrimitive || type.IsEnum || type == typeof(string);
- }
- }
- #endif
|