167 lines
8.0 KiB
C#
167 lines
8.0 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Linq.Expressions;
|
|
using System.Web.Services.Description;
|
|
using Umbraco.Core.Cache;
|
|
|
|
namespace Umbraco.Core.Dynamics
|
|
{
|
|
/// <summary>
|
|
/// Utility class for finding extension methods on a type to execute
|
|
/// </summary>
|
|
internal static class ExtensionMethodFinder
|
|
{
|
|
/// <summary>
|
|
/// The static cache for extension methods found that match the criteria that we are looking for
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<Tuple<Type, string, int>, MethodInfo[]> MethodCache = new ConcurrentDictionary<Tuple<Type, string, int>, MethodInfo[]>();
|
|
|
|
/// <summary>
|
|
/// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!!
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <remarks>
|
|
/// We cache this as a sliding 5 minute exiration, in unit tests there's over 1100 methods found, surely that will eat up a bit of memory so we want
|
|
/// to make sure we give it back.
|
|
/// </remarks>
|
|
private static IEnumerable<MethodInfo> GetAllExtensionMethodsInAppDomain(IRuntimeCacheProvider runtimeCacheProvider)
|
|
{
|
|
if (runtimeCacheProvider == null) throw new ArgumentNullException("runtimeCacheProvider");
|
|
|
|
return runtimeCacheProvider.GetCacheItem<MethodInfo[]>(typeof (ExtensionMethodFinder).Name, () => TypeFinder.GetAssembliesWithKnownExclusions()
|
|
// assemblies that contain extension methods
|
|
.Where(a => a.IsDefined(typeof (ExtensionAttribute), false))
|
|
// types that contain extension methods
|
|
.SelectMany(a => a.GetTypes()
|
|
.Where(t => t.IsDefined(typeof (ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false))
|
|
// actual extension methods
|
|
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public)
|
|
.Where(m => m.IsDefined(typeof (ExtensionAttribute), false)))
|
|
// and also IEnumerable<T> extension methods - because the assembly is excluded
|
|
.Concat(typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public))
|
|
//If we don't do this then we'll be scanning all assemblies each time!
|
|
.ToArray(),
|
|
|
|
//only cache for 5 minutes
|
|
timeout: TimeSpan.FromMinutes(5),
|
|
|
|
//each time this is accessed it will be for 5 minutes longer
|
|
isSliding:true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all extension methods found matching the definition
|
|
/// </summary>
|
|
/// <param name="runtimeCache">
|
|
/// The runtime cache is used to temporarily cache all extension methods found in the app domain so that
|
|
/// while we search for individual extension methods, the process will be reasonably 'quick'. We then statically
|
|
/// cache the MethodInfo's that we are looking for and then the runtime cache will expire and give back all that memory.
|
|
/// </param>
|
|
/// <param name="thisType"></param>
|
|
/// <param name="name"></param>
|
|
/// <param name="argumentCount">
|
|
/// The arguments EXCLUDING the 'this' argument in an extension method
|
|
/// </param>
|
|
/// <returns></returns>
|
|
/// <remarks>
|
|
/// NOTE: This will be an intensive method to call! Results will be cached based on the key (args) of this method
|
|
/// </remarks>
|
|
internal static IEnumerable<MethodInfo> GetAllExtensionMethods(IRuntimeCacheProvider runtimeCache, Type thisType, string name, int argumentCount)
|
|
{
|
|
var key = new Tuple<Type, string, int>(thisType, name, argumentCount);
|
|
|
|
return MethodCache.GetOrAdd(key, tuple =>
|
|
{
|
|
var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache);
|
|
|
|
// filter by name
|
|
var filtr1 = candidates.Where(m => m.Name == name);
|
|
|
|
// filter by args count
|
|
// ensure we add + 1 to the arg count because the 'this' arg is not included in the count above!
|
|
var filtr2 = filtr1.Where(m => m.GetParameters().Length == argumentCount + 1);
|
|
|
|
// filter by first parameter type (target of the extension method)
|
|
// ie find the right overload that can take genericParameterType
|
|
// (which will be either DynamicNodeList or List<DynamicNode> which is IEnumerable)
|
|
var filtr3 = filtr2.Select(x =>
|
|
{
|
|
var t = x.GetParameters()[0].ParameterType; // exists because of +1 above
|
|
var bindings = new Dictionary<string, Type>();
|
|
if (TypeHelper.MatchType(thisType, t, bindings) == false) return null;
|
|
|
|
// create the generic method if necessary
|
|
if (x.ContainsGenericParameters == false) return x;
|
|
var targs = t.GetGenericArguments().Select(y => bindings[y.Name]).ToArray();
|
|
return x.MakeGenericMethod(targs);
|
|
}).Where(x => x != null);
|
|
|
|
return filtr3.ToArray();
|
|
});
|
|
|
|
}
|
|
|
|
private static MethodInfo DetermineMethodFromParams(IEnumerable<MethodInfo> methods, Type genericType, IEnumerable<object> args)
|
|
{
|
|
MethodInfo methodToExecute = null;
|
|
|
|
//Given the args, lets get the types and compare the type sequence to try and find the correct overload
|
|
var argTypes = args.Select(o =>
|
|
{
|
|
var oe = (o as Expression);
|
|
return oe != null ? oe.Type : o.GetType();
|
|
});
|
|
|
|
var methodsWithArgTypes = methods.Select(method => new
|
|
{
|
|
method,
|
|
//skip the first arg because that is the extension method type ('this') that we are looking for
|
|
types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1)
|
|
});
|
|
|
|
//This type comparer will check
|
|
var typeComparer = new DelegateEqualityComparer<Type>(
|
|
//Checks if the argument type passed in can be assigned from the parameter type in the method. For
|
|
// example, if the argument type is HtmlHelper<MyModel> but the method parameter type is HtmlHelper then
|
|
// it will match because the argument is assignable to that parameter type and will be able to execute
|
|
TypeHelper.IsTypeAssignableFrom,
|
|
//This will not ever execute but if it does we need to get the hash code of the string because the hash
|
|
// code of a type is random
|
|
type => type.FullName.GetHashCode());
|
|
|
|
var firstMatchingOverload = methodsWithArgTypes
|
|
.FirstOrDefault(m => m.types.SequenceEqual(argTypes, typeComparer));
|
|
|
|
if (firstMatchingOverload != null)
|
|
{
|
|
methodToExecute = firstMatchingOverload.method;
|
|
}
|
|
|
|
return methodToExecute;
|
|
}
|
|
|
|
public static MethodInfo FindExtensionMethod(IRuntimeCacheProvider runtimeCache, Type thisType, object[] args, string name, bool argsContainsThis)
|
|
{
|
|
Type genericType = null;
|
|
if (thisType.IsGenericType)
|
|
{
|
|
genericType = thisType.GetGenericArguments()[0];
|
|
}
|
|
|
|
args = args
|
|
//if the args contains 'this', remove the first one since that is 'this' and we don't want to use
|
|
//that in the method searching
|
|
.Skip(argsContainsThis ? 1 : 0)
|
|
.ToArray();
|
|
|
|
var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray();
|
|
|
|
return DetermineMethodFromParams(methods, genericType, args);
|
|
}
|
|
}
|
|
}
|