Skip to content

Instantly share code, notes, and snippets.

@andrewmd5
Created February 6, 2022 02:46
Show Gist options
  • Save andrewmd5/7fdc3b4ce051cb288d7310f49a4f8fa9 to your computer and use it in GitHub Desktop.
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
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;
}
}
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