Created
February 6, 2022 02:46
-
-
Save andrewmd5/7fdc3b4ce051cb288d7310f49a4f8fa9 to your computer and use it in GitHub Desktop.
A rough implementation of how you can create your own methods similar to that of Minimal APIs mapping function with arbitrary delegates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Reflection; | |
using Gateway.Core.Http.Models; | |
using Microsoft.AspNetCore.Http; | |
namespace Gateway.Core.Utils; | |
internal static class DelegateBuddy | |
{ | |
private static readonly NullabilityInfoContext NullabilityContext = new(); | |
public static object?[] BindDelegateParameters(Delegate @delegate, | |
Request request, | |
Response response) | |
{ | |
// Parameters declared in route handlers are treated as required: | |
// - If a request matches the route, the route handler only runs if all required parameters are provided in the request. | |
// - Failure to provide all required parameters results in an error. | |
// - Parameters are matched by name; a route template variable or query string MUST match the declared variable name | |
var method = @delegate.Method; | |
var routeDataCollection = request.RouteDataCollection; | |
var queryCollection = request.QueryCollection; | |
var methodParameters = method.GetParameters(); | |
var final = new object?[methodParameters.Length]; | |
for (var i = 0; i < methodParameters.Length; i++) | |
{ | |
var parameter = methodParameters[i]; | |
// allows any delegate to still use the Request and Response instances | |
if (parameter.ParameterType == typeof(Request)) | |
{ | |
final[i] = request; | |
continue; | |
} | |
if (parameter.ParameterType == typeof(Response)) | |
{ | |
final[i] = response; | |
continue; | |
} | |
if (string.IsNullOrEmpty(parameter.Name)) | |
{ | |
throw new InvalidOperationException("A parameter name could not be discerned"); | |
} | |
// first we check if the parameter is defined in the route data | |
if (routeDataCollection.ContainsKey(parameter.Name)) | |
{ | |
var routeData = routeDataCollection[parameter.Name]; | |
// if it is (and a value is present) then we try to parse it and continue to the next iteration on success. | |
if (routeData.Value.TryParseValue(parameter.ParameterType, out var routeValue)) | |
{ | |
final[i] = routeValue; | |
continue; | |
} | |
} | |
// if the parameter was not in the route data, next we check the query collection. | |
if (queryCollection.ContainsKey(parameter.Name)) | |
{ | |
// as with above, if the parameter is present in the collection of query parameters, then we parse and continue on success. | |
if (queryCollection[parameter.Name].ToString().TryParseValue(parameter.ParameterType, out var queryValue)) | |
{ | |
final[i] = queryValue; | |
continue; | |
} | |
} | |
// if we're here the data is missing | |
// if the parameter is optional we have two options | |
if (parameter.IsOptional) | |
{ | |
// the first is that if it has a default value, simply set that. | |
if (parameter.HasDefaultValue) | |
{ | |
final[i] = parameter.DefaultValue; | |
continue; | |
} | |
// otherwise we can set the value of the parameter to be default(Type) | |
final[i] = GetDefault(parameter.ParameterType); | |
continue; | |
} | |
// we can also check if the parameter is nullable (which signals optionality) | |
var nullabilityInfo = NullabilityContext.Create(parameter); | |
if (nullabilityInfo.WriteState is NullabilityState.Nullable) | |
{ | |
final[i] = null; | |
continue; | |
} | |
throw new BadHttpRequestException($"Failed to bind parameter '{parameter.ParameterType.Name} {parameter.Name}'"); | |
} | |
return final; | |
} | |
public static object? GetDefault(this Type t) | |
{ | |
var defaultValue = typeof(DelegateBuddy) | |
.GetRuntimeMethod(nameof(GetDefaultGeneric), new Type[] { }) | |
?.MakeGenericMethod(t) | |
.Invoke(null, null); | |
return defaultValue; | |
} | |
public static T? GetDefaultGeneric<T>() => default; | |
private static bool TryParseValue(this string input, Type? type, out object? value) | |
{ | |
while (true) | |
{ | |
if (type is null) | |
{ | |
throw new ArgumentNullException(nameof(type)); | |
} | |
if (type == typeof(Guid) && Guid.TryParse(input, out var g)) | |
{ | |
value = g; | |
return true; | |
} | |
switch (Type.GetTypeCode(type)) | |
{ | |
case TypeCode.Empty: | |
value = null; | |
return true; | |
case TypeCode.Boolean when bool.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Char when char.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.SByte when sbyte.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Byte when byte.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Int16 when short.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.UInt16 when ushort.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Int32 when int.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.UInt32 when uint.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Int64 when long.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.UInt64 when ulong.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Single when float.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Double when double.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.Decimal when decimal.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.DateTime when DateTime.TryParse(input, out var r): | |
{ | |
value = r; | |
return true; | |
} | |
case TypeCode.String: | |
value = input; | |
return true; | |
case TypeCode.Object when type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>): | |
{ | |
type = Nullable.GetUnderlyingType(type); | |
continue; | |
} | |
default: | |
value = null; | |
return false; | |
} | |
} | |
} | |
/// <summary> | |
/// Determines if a type is numeric. | |
/// </summary> | |
/// <remarks> | |
/// Boolean is not considered numeric. Nullable numeric types are considered numeric. | |
/// </remarks> | |
public static bool IsNumericType(this Type? type) | |
{ | |
if (type is null) | |
{ | |
return false; | |
} | |
switch (Type.GetTypeCode(type)) | |
{ | |
case TypeCode.Byte: | |
case TypeCode.Decimal: | |
case TypeCode.Double: | |
case TypeCode.Int16: | |
case TypeCode.Int32: | |
case TypeCode.Int64: | |
case TypeCode.SByte: | |
case TypeCode.Single: | |
case TypeCode.UInt16: | |
case TypeCode.UInt32: | |
case TypeCode.UInt64: | |
return true; | |
case TypeCode.Object: | |
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) | |
{ | |
return IsNumericType(Nullable.GetUnderlyingType(type)); | |
} | |
return false; | |
} | |
return false; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Reflection; | |
using System.Runtime.CompilerServices; | |
using Microsoft.Extensions.Logging; | |
namespace Gateway.Core.Http.Routing; | |
/// <summary> | |
/// Intercepts method execution of dynamically bound .NET delegates | |
/// </summary> | |
internal class ExecutionInterceptor | |
{ | |
private static readonly Type TypeOfVoidTaskResult = Type.GetType("System.Threading.Tasks.VoidTaskResult") ?? throw new InvalidOperationException(); | |
private static readonly Type TypeOfTaskOfVoidTaskResult = typeof(Task<>).MakeGenericType(TypeOfVoidTaskResult); | |
public ExecutionInterceptor(ILogger logger) | |
{ | |
_logger = logger; | |
} | |
private readonly ILogger _logger; | |
/// <summary> | |
/// Executes the specified <paramref name="delegate"/> with the provided <paramref name="parameters"/> and intercepts | |
/// its return value (if any). | |
/// </summary> | |
/// <param name="delegate">A delegate that will will dynamically invoked</param> | |
/// <param name="parameters">parameters to be passed to <paramref name="delegate"/></param> | |
/// <returns>The results of the invoked delegate, if any.</returns> | |
public async ValueTask<object?> Intercept(Delegate @delegate, params object?[]? parameters) | |
{ | |
// execute the delegate using the provided parameters | |
var returnValue = @delegate.DynamicInvoke(parameters); | |
// there is no return value, so return immediately. | |
if (returnValue is null) | |
{ | |
return null; | |
} | |
var returnType = returnValue.GetType(); | |
if (IsAwaitableTask(returnType)) | |
{ | |
await (dynamic)returnValue; | |
return null; | |
} | |
if (IsAwaitableTaskResult(returnType) || IsAsyncStateMachineBox(returnType)) | |
{ | |
return await ConvertTask((Task)returnValue); | |
} | |
return returnValue; | |
} | |
/// <summary> | |
/// Converts an Object into a runnable task. | |
/// </summary> | |
/// <param name="task">the task that will be executed via Reflection</param> | |
/// <returns>the task results, if any.</returns> | |
private static async Task<object?> ConvertTask(Task task) | |
{ | |
// run the task | |
await task; | |
// if the task is a void, we can just return. | |
if (TypeOfTaskOfVoidTaskResult.IsInstanceOfType(task)) | |
{ | |
return null; | |
} | |
// now use reflection to get the results | |
var property = task.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance); | |
// grab the actual current return value from memory | |
return property?.GetValue(task, null); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] | |
private static bool IsAwaitableTask(Type type) => type == typeof(Task) | |
|| type == typeof(ValueTask) | |
|| type == TypeOfTaskOfVoidTaskResult; | |
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] | |
private static bool IsAwaitableTaskResult(Type type) => type.IsGenericType | |
&& type.GetGenericTypeDefinition() is var genericType | |
&& (genericType == typeof(Task<>) || genericType == typeof(ValueTask<>)); | |
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] | |
private static bool IsAsyncStateMachineBox(Type type) => type.Name.Contains("AsyncStateMachineBox"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment