From f037f8541caf7d2cda10c697e10d863cc4d067cf Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jan 2015 19:04:39 +1100 Subject: [PATCH] Fixes: U4-6210 ExtensionMethodFinder doesn't work with overloaded extensions - this means overloaded extension methods for dynamic content won't work. and finalizes: U4-6209 GetGridHtml needs to be an extension on HtmlHelper not IPublishedContent so that ViewData and ModelState are inherited --- .../Dynamics/DynamicInstanceHelper.cs | 104 +++--- src/Umbraco.Core/Dynamics/DynamicXml.cs | 5 +- .../Dynamics/ExtensionMethodFinder.cs | 303 +++++++----------- .../ExtensionMethodFinderTests.cs | 112 ++++++- src/Umbraco.Web/Dynamics/ExpressionParser.cs | 11 +- src/Umbraco.Web/GridTemplateExtensions.cs | 109 ++++--- .../Models/DynamicPublishedContent.cs | 9 +- .../Models/DynamicPublishedContentList.cs | 5 +- .../RazorDynamicNode/ExtensionMethodFinder.cs | 6 +- 9 files changed, 362 insertions(+), 302 deletions(-) diff --git a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs index 95ed389a7a..94394abb3e 100644 --- a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs +++ b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs @@ -5,6 +5,7 @@ using System.Dynamic; using System.Linq; using System.Reflection; using System.Text; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; namespace Umbraco.Core.Dynamics @@ -34,38 +35,42 @@ namespace Umbraco.Core.Dynamics FoundExtensionMethod } - /// - /// Attempts to invoke a member based on the dynamic instance - /// - /// - /// The object instance to invoke the extension method for - /// - /// - /// - /// - /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method - /// on the object that matches the binder name - /// - public static Attempt TryInvokeMember(T thisObject, InvokeMemberBinder binder, object[] args) + /// + /// Attempts to invoke a member based on the dynamic instance + /// + /// + /// + /// The object instance to invoke the extension method for + /// + /// + /// + /// + /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method + /// on the object that matches the binder name + /// + public static Attempt TryInvokeMember(IRuntimeCacheProvider runtimeCache, T thisObject, InvokeMemberBinder binder, object[] args) { - return TryInvokeMember(thisObject, binder, args, null); + return TryInvokeMember(runtimeCache, thisObject, binder, args, null); } - /// - /// Attempts to invoke a member based on the dynamic instance - /// - /// - /// The object instance to invoke the extension method for - /// - /// - /// The types to scan for extension methods - /// - /// - /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method - /// on the object that matches the binder name, if that fails it will then attempt to invoke an extension method - /// based on the binder name and the extension method types to scan. - /// - public static Attempt TryInvokeMember(T thisObject, + /// + /// Attempts to invoke a member based on the dynamic instance + /// + /// + /// + /// The object instance to invoke the extension method for + /// + /// + /// The types to scan for extension methods + /// + /// + /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method + /// on the object that matches the binder name, if that fails it will then attempt to invoke an extension method + /// based on the binder name and the extension method types to scan. + /// + public static Attempt TryInvokeMember( + IRuntimeCacheProvider runtimeCache, + T thisObject, InvokeMemberBinder binder, object[] args, IEnumerable findExtensionMethodsOnTypes) @@ -76,9 +81,9 @@ namespace Umbraco.Core.Dynamics { //Property? result = typeof(T).InvokeMember(binder.Name, - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.GetProperty, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.GetProperty, null, thisObject, args); @@ -90,10 +95,10 @@ namespace Umbraco.Core.Dynamics { //Static or Instance Method? result = typeof(T).InvokeMember(binder.Name, - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.Static | - System.Reflection.BindingFlags.InvokeMethod, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.Static | + BindingFlags.InvokeMethod, null, thisObject, args); @@ -105,7 +110,7 @@ namespace Umbraco.Core.Dynamics { try { - result = FindAndExecuteExtensionMethod(thisObject, args, binder.Name, findExtensionMethodsOnTypes); + result = FindAndExecuteExtensionMethod(runtimeCache, thisObject, args, binder.Name, findExtensionMethodsOnTypes); return Attempt.Succeed(new TryInvokeMemberResult(result, TryInvokeMemberSuccessReason.FoundExtensionMethod)); } catch (TargetInvocationException ext) @@ -138,16 +143,19 @@ namespace Umbraco.Core.Dynamics } } - /// - /// Attempts to find an extension method that matches the name and arguments based on scanning the Type's passed in - /// to the findMethodsOnTypes parameter - /// - /// The instance object to execute the extension method for - /// - /// - /// - /// - internal static object FindAndExecuteExtensionMethod(T thisObject, + /// + /// Attempts to find an extension method that matches the name and arguments based on scanning the Type's passed in + /// to the findMethodsOnTypes parameter + /// + /// + /// The instance object to execute the extension method for + /// + /// + /// + /// + internal static object FindAndExecuteExtensionMethod( + IRuntimeCacheProvider runtimeCache, + T thisObject, object[] args, string name, IEnumerable findMethodsOnTypes) @@ -158,7 +166,7 @@ namespace Umbraco.Core.Dynamics MethodInfo toExecute = null; foreach (var t in findMethodsOnTypes) { - toExecute = ExtensionMethodFinder.FindExtensionMethod(t, args, name, false); + toExecute = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, t, args, name, false); if (toExecute != null) break; } diff --git a/src/Umbraco.Core/Dynamics/DynamicXml.cs b/src/Umbraco.Core/Dynamics/DynamicXml.cs index 7a4d714fbe..5fd97fc778 100644 --- a/src/Umbraco.Core/Dynamics/DynamicXml.cs +++ b/src/Umbraco.Core/Dynamics/DynamicXml.cs @@ -10,6 +10,7 @@ using System.Collections; using System.IO; using System.Web; using Umbraco.Core; +using Umbraco.Core.Cache; namespace Umbraco.Core.Dynamics { @@ -162,8 +163,10 @@ namespace Umbraco.Core.Dynamics return true; //anyway } + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + //ok, now lets try to match by member, property, extensino method - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof (IEnumerable), typeof (IEnumerable), diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index cfc8a92ddf..fbfa933e83 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Web.Compilation; using System.Runtime.CompilerServices; -using System.Collections; using System.Linq.Expressions; +using Umbraco.Core.Cache; namespace Umbraco.Core.Dynamics { @@ -14,148 +14,89 @@ namespace Umbraco.Core.Dynamics /// internal static class ExtensionMethodFinder { - private static readonly MethodInfo[] AllExtensionMethods; + /// + /// The static cache for extension methods found that match the criteria that we are looking for + /// + private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); - static ExtensionMethodFinder() - { - AllExtensionMethods = TypeFinder.GetAssembliesWithKnownExclusions() + /// + /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! + /// + /// + /// + /// 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. + /// + private static IEnumerable GetAllExtensionMethodsInAppDomain(IRuntimeCacheProvider runtimeCacheProvider) + { + if (runtimeCacheProvider == null) throw new ArgumentNullException("runtimeCacheProvider"); + + return runtimeCacheProvider.GetCacheItem(typeof (ExtensionMethodFinder).Name, () => TypeFinder.GetAssembliesWithKnownExclusions() // assemblies that contain extension methods - .Where(a => a.IsDefined(typeof(ExtensionAttribute), false)) + .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)) + .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))) + .Where(m => m.IsDefined(typeof (ExtensionAttribute), false))) // and also IEnumerable extension methods - because the assembly is excluded - .Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) - .ToArray(); - } + .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), - // ORIGINAL CODE IS NOT COMPLETE, DOES NOT HANDLE GENERICS, ETC... - - // so this is an attempt at fixing things, but it's not done yet - // and do we really want to do this? extension methods are not supported on dynamics, period - // we should use strongly typed content instead of dynamics. - - /* - - // get all extension methods for type thisType, with name name, - // accepting argsCount arguments (not counting the instance of thisType). - private static IEnumerable GetExtensionMethods(Type thisType, string name, int argsCount) - { - var key = string.Format("{0}.{1}::{2}", thisType.FullName, name, argsCount); - - var types = thisType.GetBaseTypes(true); // either do this OR have MatchFirstParameter handle the stuff... F*XME - - var methods = AllExtensionMethods - .Where(m => m.Name == name) - .Where(m => m.GetParameters().Length == argsCount) - .Where(m => MatchFirstParameter(thisType, m.GetParameters()[0].ParameterType)); - - // f*xme - is this what we should cache? - return methods; + //each time this is accessed it will be for 5 minutes longer + isSliding:true); } - // find out whether the first parameter is a match for thisType - private static bool MatchFirstParameter(Type thisType, Type firstParameterType) - { - return MethodArgZeroHasCorrectTargetType(null, firstParameterType, thisType); - } + /// + /// Returns all extension methods found matching the definition + /// + /// + /// 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. + /// + /// + /// + /// + /// The arguments EXCLUDING the 'this' argument in an extension method + /// + /// + /// + /// NOTE: This will be an intensive method to call! Results will be cached based on the key (args) of this method + /// + internal static IEnumerable GetAllExtensionMethods(IRuntimeCacheProvider runtimeCache, Type thisType, string name, int argumentCount) + { + var key = new Tuple(thisType, name, argumentCount); - // get the single extension method for type thisType, with name name, - // that accepts the arguments in args (which does not contain the instance of thisType). - public static MethodInfo GetExtensionMethod(Type thisType, string name, object[] args) - { - MethodInfo result = null; - foreach (var method in GetExtensionMethods(thisType, name, args.Length).Where(m => MatchParameters(m, args))) - { - if (result == null) - result = method; - else - throw new AmbiguousMatchException("More than one matching extension method was found."); - } - return result; - } + return MethodCache.GetOrAdd(key, tuple => + { + var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // find out whether the method can accept the arguments - private static bool MatchParameters(MethodInfo method, IList args) - { - var parameters = method.GetParameters(); - var i = 0; - for (; i < parameters.Length; ++i) - { - if (MatchParameter(parameters[i].ParameterType, args[i].GetType()) == false) - break; - } - return (i == parameters.Length); - } + //filter by name + var methodsByName = candidates.Where(m => m.Name == name); - internal static bool MatchParameter(Type parameterType, Type argumentType) - { - // public static int DoSomething(Foo foo, T t1, T t2) - // DoSomething(foo, t1, t2) => how can we match?! - return parameterType == argumentType; // f*xme of course! - } - * - */ + //ensure we add + 1 to the arg count because the 'this' arg is not included in the count above! + var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + 1); - // BELOW IS THE ORIGINAL CODE... + //find the right overload that can take genericParameterType + //which will be either DynamicNodeList or List which is IEnumerable` - /// - /// Returns all extension methods found matching the definition - /// - /// - /// - /// - /// - /// - /// - /// TODO: NOTE: This will be an intensive method to call!! Results should be cached! - /// - private static IEnumerable GetAllExtensionMethods(Type thisType, string name, int argumentCount, bool argsContainsThis) - { - // at *least* we can cache the extension methods discovery - var candidates = AllExtensionMethods; + var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - /* - //only scan assemblies we know to contain extension methods (user assemblies) - var assembliesToScan = TypeFinder.GetAssembliesWithKnownExclusions(); + var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType + where + method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) + select method); - //get extension methods from runtime - var candidates = ( - from assembly in assembliesToScan - where assembly.IsDefined(typeof(ExtensionAttribute), false) - from type in assembly.GetTypes() - where (type.IsDefined(typeof(ExtensionAttribute), false) - && type.IsSealed && !type.IsGenericType && !type.IsNested) - from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public) - // this filters extension methods - where method.IsDefined(typeof(ExtensionAttribute), false) - select method - ); - - //add the extension methods defined in IEnumerable - candidates = candidates.Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)); - */ - - //filter by name - var methodsByName = candidates.Where(m => m.Name == name); - - var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + (argsContainsThis ? 0 : 1)); - - //find the right overload that can take genericParameterType - //which will be either DynamicNodeList or List which is IEnumerable` - - var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - - var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType - where - method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) - select method); - - return methodsWhereArgZeroIsTargetType.Select(mt => mt.m); + return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); + }); + } private static bool MethodArgZeroHasCorrectTargetType(MethodInfo method, Type firstArgumentType, Type thisType) @@ -182,36 +123,36 @@ namespace Umbraco.Core.Dynamics private static bool MethodArgZeroHasCorrectTargetTypeShareACommonInterface(MethodInfo method, Type firstArgumentType, Type thisType) { - Type[] interfaces = firstArgumentType.GetInterfaces(); + var interfaces = firstArgumentType.GetInterfaces(); if (interfaces.Length == 0) { return false; } - bool result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); + var result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); return result; } private static bool MethodArgZeroHasCorrectTargetTypeIsASubclassOf(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.IsSubclassOf(firstArgumentType); + var result = thisType.IsSubclassOf(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeAnInterfaceMatches(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.GetInterfaces().Contains(firstArgumentType); + var result = thisType.GetInterfaces().Contains(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeTypeMatchesExactly(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = (thisType == firstArgumentType); + var result = (thisType == firstArgumentType); return result; } private static Type FirstParameterType(MethodInfo m) { - ParameterInfo[] p = m.GetParameters(); + var p = m.GetParameters(); if (p.Any()) { return p.First().ParameterType; @@ -219,74 +160,62 @@ namespace Umbraco.Core.Dynamics return null; } - private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) + private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) { - if (!methods.Any()) - { - return null; - } MethodInfo methodToExecute = null; - if (methods.Count() > 1) - { - //Given the args, lets get the types and compare the type sequence to try and find the correct overload - var argTypes = args.ToList().ConvertAll(o => - { - var oe = (o as Expression); - if (oe != null) - { - return oe.Type.FullName; - } - return o.GetType().FullName; - }); - var methodsWithArgTypes = methods.Select(method => new { method, types = method.GetParameters().Select(pi => pi.ParameterType.FullName) }); - var firstMatchingOverload = methodsWithArgTypes.FirstOrDefault(m => m.types.SequenceEqual(argTypes)); - if (firstMatchingOverload != null) - { - methodToExecute = firstMatchingOverload.method; - } - } - if (methodToExecute == null) - { - var firstMethod = methods.FirstOrDefault(); - // NH: this is to ensure that it's always the correct one being chosen when using the LINQ extension methods - if (methods.Count() > 1) - { - var firstGenericMethod = methods.FirstOrDefault(x => x.IsGenericMethodDefinition); - if (firstGenericMethod != null) - { - firstMethod = firstGenericMethod; - } - } + //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( + //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 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; + } - if (firstMethod != null) - { - if (firstMethod.IsGenericMethodDefinition) - { - if (genericType != null) - { - methodToExecute = firstMethod.MakeGenericMethod(genericType); - } - } - else - { - methodToExecute = firstMethod; - } - } - } return methodToExecute; } - public static MethodInfo FindExtensionMethod(Type thisType, object[] args, string name, bool argsContainsThis) + public static MethodInfo FindExtensionMethod(IRuntimeCacheProvider runtimeCache, Type thisType, object[] args, string name, bool argsContainsThis) { Type genericType = null; if (thisType.IsGenericType) { genericType = thisType.GetGenericArguments()[0]; - } + } - var methods = GetAllExtensionMethods(thisType, name, args.Length, argsContainsThis).ToArray(); - return DetermineMethodFromParams(methods, genericType, args); + 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); } } } diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs index 56fc36a123..37b0a63866 100644 --- a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Web.Mvc; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; namespace Umbraco.Tests.DynamicsAndReflection @@ -27,8 +29,8 @@ namespace Umbraco.Tests.DynamicsAndReflection // // Eric Lippert, http://stackoverflow.com/questions/5311465/extension-method-and-dynamic-object-in-c-sharp + [Ignore("This is just testing the below GetMethodForArguments method - Stephen was working on this but it's not used in the core")] [Test] - [Ignore("fails")] public void TypesTests() { Assert.IsTrue(typeof(int[]).Inherits()); @@ -84,7 +86,9 @@ namespace Umbraco.Tests.DynamicsAndReflection Assert.IsNotNull(m5A); // note - should we also handle "ref" and "out" parameters? + // SD: NO, lets not make this more complicated than it already is // note - should we pay attention to array types? + // SD: NO, lets not make this more complicated than it already is } public void TestMethod1(int value) {} @@ -157,42 +161,122 @@ namespace Umbraco.Tests.DynamicsAndReflection : method.MakeGenericMethod(genericArgumentTypes); } - public class Class1 + public class TestClass {} + public class TestClass : TestClass { } + [Test] - [Ignore("fails")] - public void FinderTests() + public void Find_Non_Overloaded_Method() { MethodInfo method; - var class1 = new Class1(); + var class1 = new TestClass(); - method = ExtensionMethodFinder.FindExtensionMethod(typeof (Class1), new object[] {1}, "TestMethod1", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod1", false); - Assert.IsNull(method); // note - fails, return TestMethod1! + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleMethod", false); + Assert.IsNull(method); + } - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { 1 }, "TestMethod2", false); + [Test] + public void Find_Overloaded_Method() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); - method.Invoke(null, new object[] { class1, "1" }); + method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod2", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, "x" }); } + + [Test] + public void Find_Overloaded_Method_With_Args_Containing_This() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, 1 }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, "x" }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Non_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleEnumerableGenericMethod", false); + Assert.IsNull(method); + } + + [Test] + public void Find_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Method_With_Parameter_Match_With_Generic_Argument() + { + MethodInfo method; + + var genericTestClass = new TestClass(); + var nonGenericTestClass = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { genericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { nonGenericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + } } static class ExtensionMethodFinderTestsExtensions { - public static void TestMethod1(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, string value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, string value) { } + + public static void SimpleEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, string value) + { } + + public static void GenericParameterMethod(this ExtensionMethodFinderTests.TestClass source, ExtensionMethodFinderTests.TestClass value) + { } + } } diff --git a/src/Umbraco.Web/Dynamics/ExpressionParser.cs b/src/Umbraco.Web/Dynamics/ExpressionParser.cs index d348682428..6ca357a996 100644 --- a/src/Umbraco.Web/Dynamics/ExpressionParser.cs +++ b/src/Umbraco.Web/Dynamics/ExpressionParser.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using Umbraco.Web.Models; @@ -907,10 +908,12 @@ namespace Umbraco.Web.Dynamics //SD: I have yet to see extension methods actually being called in the dynamic parsing... need to unit test these // scenarios and figure out why all this type checking occurs. + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + if (type == typeof(string) && instanceAsString != null) { Expression[] newArgs = (new List() { Expression.Invoke(instanceAsString, instanceExpression) }).Concat(args).ToArray(); - mb = ExtensionMethodFinder.FindExtensionMethod(typeof(string), newArgs, id, true); + mb = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, typeof(string), newArgs, id, true); if (mb != null) { return CallMethodOnDynamicNode(instance, newArgs, instanceAsString, instanceExpression, (MethodInfo)mb, true); @@ -919,7 +922,7 @@ namespace Umbraco.Web.Dynamics if (type == typeof(string) && instanceAsString == null && instance is MemberExpression) { Expression[] newArgs = (new List() { instance }).Concat(args).ToArray(); - mb = ExtensionMethodFinder.FindExtensionMethod(typeof(string), newArgs, id, true); + mb = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, typeof(string), newArgs, id, true); if (mb != null) { return Expression.Call(null, (MethodInfo)mb, newArgs); @@ -994,7 +997,7 @@ namespace Umbraco.Web.Dynamics //e.g. uBlogsyPostDate.Date //SD: Removed the NonPublic accessor here because this will never work in medium trust, wondering why it is NonPublic vs Public ? Have changed to Public. //MethodInfo ReflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo ReflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.Public | BindingFlags.Static); + MethodInfo reflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.Public | BindingFlags.Static); ParameterExpression convertDynamicNullToBooleanFalse = Expression.Parameter(typeof(bool), "convertDynamicNullToBooleanFalse"); ParameterExpression result = Expression.Parameter(typeof(object), "result"); ParameterExpression idParam = Expression.Parameter(typeof(string), "id"); @@ -1008,7 +1011,7 @@ namespace Umbraco.Web.Dynamics new[] { lambdaResult, result, idParam, convertDynamicNullToBooleanFalse }, Expression.Assign(convertDynamicNullToBooleanFalse, Expression.Constant(_flagConvertDynamicNullToBooleanFalse, typeof(bool))), Expression.Assign(lambdaResult, Expression.Invoke(instance, lambdaInstanceExpression)), - Expression.Assign(result, Expression.Call(ReflectPropertyValue, lambdaResult, Expression.Constant(id))), + Expression.Assign(result, Expression.Call(reflectPropertyValue, lambdaResult, Expression.Constant(id))), Expression.IfThen( Expression.AndAlso( Expression.TypeEqual(result, typeof(DynamicNull)), diff --git a/src/Umbraco.Web/GridTemplateExtensions.cs b/src/Umbraco.Web/GridTemplateExtensions.cs index d26fb1ffeb..e4af759c88 100644 --- a/src/Umbraco.Web/GridTemplateExtensions.cs +++ b/src/Umbraco.Web/GridTemplateExtensions.cs @@ -48,38 +48,30 @@ namespace Umbraco.Web var model = prop.Value; var asString = model as string; - if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); return html.Partial(view, model); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3") + public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = "bootstrap3") { var asString = property.Value as string; if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); - + var view = "Grid/" + framework; - return new MvcHtmlString(RenderPartialViewToString(view, property.Value)); + return html.Partial(view, property.Value); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html) { - return GetGridHtml(contentItem, "bodyText", "bootstrap3"); + return GetGridHtml(contentItem, html, "bodyText", "bootstrap3"); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); - return GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3"); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string framework) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); @@ -89,41 +81,68 @@ namespace Umbraco.Web var model = prop.Value; var asString = model as string; - if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); - return new MvcHtmlString(RenderPartialViewToString(view, model)); + return html.Partial(view, model); } - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - private static string RenderPartialViewToString(string viewName, object model) + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3") { + var asString = property.Value as string; + if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); - using (var sw = new StringWriter()) + var htmlHelper = CreateHtmlHelper(property.Value); + return htmlHelper.GetGridHtml(property, framework); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem) + { + return GetGridHtml(contentItem, "bodyText", "bootstrap3"); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias) + { + Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); + + return GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework) + { + Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); + + var prop = contentItem.GetProperty(propertyAlias); + if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias); + var model = prop.Value; + + var asString = model as string; + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); + + var htmlHelper = CreateHtmlHelper(model); + return htmlHelper.GetGridHtml(contentItem, propertyAlias, framework); + } + + //[Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] + private static HtmlHelper CreateHtmlHelper(object model) + { + var cc = new ControllerContext { - var cc = new ControllerContext - { - RequestContext = - new RequestContext( - UmbracoContext.Current.HttpContext, - new RouteData() { Route = RouteTable.Routes["Umbraco_default"] }) - }; + RequestContext = UmbracoContext.Current.HttpContext.Request.RequestContext + }; + var viewContext = new ViewContext(cc, new FakeView(), new ViewDataDictionary(model), new TempDataDictionary(), new StringWriter()); + var htmlHelper = new HtmlHelper(viewContext, new ViewPage()); + return htmlHelper; + } - var routeHandler = new RenderRouteHandler(ControllerBuilder.Current.GetControllerFactory(), UmbracoContext.Current); - var routeDef = routeHandler.GetUmbracoRouteDefinition(cc.RequestContext, UmbracoContext.Current.PublishedContentRequest); - cc.RequestContext.RouteData.Values.Add("action", routeDef.ActionName); - cc.RequestContext.RouteData.Values.Add("controller", routeDef.ControllerName); - - var partialView = ViewEngines.Engines.FindPartialView(cc, viewName); - var viewData = new ViewDataDictionary(); - var tempData = new TempDataDictionary(); - - viewData.Model = model; - - var viewContext = new ViewContext(cc, partialView.View, viewData, tempData, sw); - partialView.View.Render(viewContext, sw); - partialView.ViewEngine.ReleaseView(cc, partialView.View); - - return sw.GetStringBuilder().ToString(); + private class FakeView : IView + { + public void Render(ViewContext viewContext, TextWriter writer) + { } } } diff --git a/src/Umbraco.Web/Models/DynamicPublishedContent.cs b/src/Umbraco.Web/Models/DynamicPublishedContent.cs index 8832086a31..83874623b8 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContent.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContent.cs @@ -10,6 +10,7 @@ using System.Dynamic; using System.Linq; using System.Runtime.CompilerServices; using System.Web; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using Umbraco.Core; @@ -57,7 +58,11 @@ namespace Umbraco.Web.Models // these two here have leaked in v6 and so we cannot remove them anymore // without breaking compatibility but... TODO: remove them in v7 + + [Obsolete("Will be removing in future versions")] public DynamicPublishedContentList ChildrenAsList { get { return Children; } } + + [Obsolete("Will be removing in future versions")] public int parentId { get { return PublishedContent.Parent.Id; } } #region DynamicObject @@ -73,7 +78,9 @@ namespace Umbraco.Web.Models /// public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof(DynamicPublishedContent) }); diff --git a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs index 386f7c7032..ef794737a5 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Dynamic; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using System.Collections; using System.Reflection; @@ -284,8 +285,10 @@ namespace Umbraco.Web.Models return true; } + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + //ok, now lets try to match by member, property, extensino method - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof (IEnumerable), typeof (DynamicPublishedContentList) diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs b/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs index 9030cecde2..7265be6423 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs @@ -7,6 +7,8 @@ using System.Web.Compilation; using System.Runtime.CompilerServices; using System.Collections; using System.Linq.Expressions; +using Umbraco.Core; +using Umbraco.Core.Cache; namespace umbraco.MacroEngines { @@ -16,7 +18,9 @@ namespace umbraco.MacroEngines { public static MethodInfo FindExtensionMethod(Type thisType, object[] args, string name, bool argsContainsThis) { - return Umbraco.Core.Dynamics.ExtensionMethodFinder.FindExtensionMethod(thisType, args, name, argsContainsThis); + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + + return Umbraco.Core.Dynamics.ExtensionMethodFinder.FindExtensionMethod(runtimeCache, thisType, args, name, argsContainsThis); } } }