From 839a5b13b0da9069cb06e48f3ba40e3c7cda2d98 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Feb 2015 20:23:05 +1100 Subject: [PATCH 1/3] updates extension method finder with tests to better support finding generic equivialents, adds tests for TypeHelper as well. --- .../Dynamics/ExtensionMethodFinder.cs | 111 ++++++------------ src/Umbraco.Core/TypeHelper.cs | 77 +++++++++++- .../ExtensionMethodFinderTests.cs | 25 ++++ src/Umbraco.Tests/Plugins/TypeHelperTests.cs | 72 ++++++++++++ 4 files changed, 212 insertions(+), 73 deletions(-) diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index fbfa933e83..78bf778fba 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -82,75 +82,34 @@ namespace Umbraco.Core.Dynamics var methodsByName = candidates.Where(m => m.Name == name); //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); + var byParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + 1); - //find the right overload that can take genericParameterType - //which will be either DynamicNodeList or List which is IEnumerable` + var methodsWithFirstParamType = byParamCount.Select(m => new {m, t = FirstParameterType(m)}) + .ToArray(); // get the array so we don't keep enumerating. - var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); + //Find the method with an assignable first parameter type + var methodsWhereArgZeroIsTargetType = (from method in methodsWithFirstParamType + where + method.t != null && TypeHelper.IsTypeAssignableFrom(method.t, thisType) + select method).ToArray(); + + //found some so return this + if (methodsWhereArgZeroIsTargetType.Any()) return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); - var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType - where - method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) - select method); + //this is where it gets tricky, none of the first parameters were assignable to our type which means that + // if they are assignable they are generic arguments - return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); + methodsWhereArgZeroIsTargetType = (from method in methodsWithFirstParamType + where + method.t != null && TypeHelper.IsAssignableToGenericType(method.t, thisType) + select method).ToArray(); + + return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); }); } - - private static bool MethodArgZeroHasCorrectTargetType(MethodInfo method, Type firstArgumentType, Type thisType) - { - //This is done with seperate method calls because you can't debug/watch lamdas - if you're trying to figure - //out why the wrong method is returned, it helps to be able to see each boolean result - - return - - // is it defined on me? - MethodArgZeroHasCorrectTargetTypeTypeMatchesExactly(method, firstArgumentType, thisType) || - - // or on any of my interfaces? - MethodArgZeroHasCorrectTargetTypeAnInterfaceMatches(method, firstArgumentType, thisType) || - - // or on any of my base types? - MethodArgZeroHasCorrectTargetTypeIsASubclassOf(method, firstArgumentType, thisType) || - - //share a common interface (e.g. IEnumerable) - MethodArgZeroHasCorrectTargetTypeShareACommonInterface(method, firstArgumentType, thisType); - - - } - - private static bool MethodArgZeroHasCorrectTargetTypeShareACommonInterface(MethodInfo method, Type firstArgumentType, Type thisType) - { - var interfaces = firstArgumentType.GetInterfaces(); - if (interfaces.Length == 0) - { - return false; - } - var result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); - return result; - } - - private static bool MethodArgZeroHasCorrectTargetTypeIsASubclassOf(MethodInfo method, Type firstArgumentType, Type thisType) - { - var result = thisType.IsSubclassOf(firstArgumentType); - return result; - } - - private static bool MethodArgZeroHasCorrectTargetTypeAnInterfaceMatches(MethodInfo method, Type firstArgumentType, Type thisType) - { - var result = thisType.GetInterfaces().Contains(firstArgumentType); - return result; - } - - private static bool MethodArgZeroHasCorrectTargetTypeTypeMatchesExactly(MethodInfo method, Type firstArgumentType, Type thisType) - { - var result = (thisType == firstArgumentType); - return result; - } - - private static Type FirstParameterType(MethodInfo m) + + private static Type FirstParameterType(MethodInfo m) { var p = m.GetParameters(); if (p.Any()) @@ -160,7 +119,7 @@ namespace Umbraco.Core.Dynamics return null; } - private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) + private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type thisType, IEnumerable args) { MethodInfo methodToExecute = null; @@ -194,19 +153,27 @@ namespace Umbraco.Core.Dynamics if (firstMatchingOverload != null) { methodToExecute = firstMatchingOverload.method; + + var extensionParam = methodToExecute.GetParameters()[0].ParameterType; + + //We've already done this check before in the GetAllExtensionMethods method, but need to do it here + // again because in order to use this found generic method we need to create a real generic method + // with the generic type parameters + var baseCompareTypeAttempt = TypeHelper.IsAssignableToGenericType(extensionParam, thisType); + //This should never throw + if (baseCompareTypeAttempt.Success == false) throw new InvalidOperationException("No base compare type could be resolved"); + + if (methodToExecute.IsGenericMethodDefinition && baseCompareTypeAttempt.Result.IsGenericType) + { + methodToExecute = methodToExecute.MakeGenericMethod(baseCompareTypeAttempt.Result.GetGenericArguments()); + } } - return methodToExecute; + 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 @@ -214,8 +181,8 @@ namespace Umbraco.Core.Dynamics .ToArray(); var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); - - return DetermineMethodFromParams(methods, genericType, args); + + return DetermineMethodFromParams(methods, thisType, args); } } } diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index 6a35c92f08..03e5769c1c 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -131,7 +131,82 @@ namespace Umbraco.Core return Attempt.Fail(); } - /// + /// + /// Checks if the generic type passed in can be assigned from the given type + /// + /// + /// + /// + /// Returns an Attempt{Type} which if true will include the actual type that matched the genericType + /// being compared. + /// + /// + /// First we need to check a special case, if the generic type is a generic definition but has not FullName, then + /// we cannot compare it with traditional means because the types will never match. + /// Generic types will not have a FullName in these cases: http://blogs.msdn.com/b/haibo_luo/archive/2006/02/17/534480.aspx + /// or apparently when you retrieve a generic method parameter using reflection. + /// + /// This is using a version modified from: http://stackoverflow.com/a/1075059/1968 + /// + public static Attempt IsAssignableToGenericType(Type genericType, Type givenType) + { + if (genericType.IsGenericTypeDefinition == false && genericType.FullName.IsNullOrWhiteSpace()) + { + return IsAssignableToReflectedGenericType(genericType, givenType); + } + + var genericTypeDef = givenType.IsGenericType ? givenType.GetGenericTypeDefinition() : null; + + if (genericTypeDef != null && genericTypeDef == genericType) + return Attempt.Succeed(givenType); + + var its = givenType.GetInterfaces(); + + foreach (var it in its) + { + genericTypeDef = it.IsGenericType ? it.GetGenericTypeDefinition() : null; + + if (genericTypeDef != null && genericTypeDef == genericType) + return Attempt.Succeed(it); + } + + var baseType = givenType.BaseType; + return baseType != null + ? IsAssignableToGenericType(genericType, baseType) + : Attempt.Fail(); + } + + /// + /// This is used in IsAssignableToGenericType + /// + /// + /// + /// + /// Returns an Attempt{Type} which if true will include the actual type that matched the genericType + /// being compared. + /// + private static Attempt IsAssignableToReflectedGenericType(Type genericType, Type givenType) + { + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition().Name == genericType.Name && givenType.GenericTypeArguments.Length == genericType.GenericTypeArguments.Length) + return Attempt.Succeed(givenType); + + var its = givenType.GetInterfaces(); + + foreach (var it in its) + { + if (it.IsGenericType && it.GetGenericTypeDefinition().Name == genericType.Name && givenType.GenericTypeArguments.Length == genericType.GenericTypeArguments.Length) + { + return Attempt.Succeed(it); + } + } + + var baseType = givenType.BaseType; + return baseType != null + ? IsAssignableToReflectedGenericType(genericType, baseType) + : Attempt.Fail(); + } + + /// /// Determines whether the type is assignable from the specified implementation , /// and caches the result across the application using a . /// diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs index 37b0a63866..15844e4245 100644 --- a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -166,6 +166,11 @@ namespace Umbraco.Tests.DynamicsAndReflection public class TestClass : TestClass { } + public class TestClassCollection : List + { + + } + [Test] public void Find_Non_Overloaded_Method() { @@ -253,10 +258,30 @@ namespace Umbraco.Tests.DynamicsAndReflection method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { nonGenericTestClass }, "GenericParameterMethod", false); Assert.IsNotNull(method); } + + [Test] + public void Find_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] {}, "GenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1 }); + + var class2 = new TestClassCollection(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClassCollection), new object[] { }, "GenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class2 }); + } } static class ExtensionMethodFinderTestsExtensions { + public static void GenericMethod(this IEnumerable source) + { } + public static void SimpleMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } diff --git a/src/Umbraco.Tests/Plugins/TypeHelperTests.cs b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs index 404dc00eaa..d66c06efda 100644 --- a/src/Umbraco.Tests/Plugins/TypeHelperTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs @@ -1,11 +1,17 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.Data.Odbc; using System.Data.OleDb; using System.Data.SqlClient; +using System.Linq; +using System.Reflection; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Web.Cache; +using Umbraco.Web.Models; using Umbraco.Web.Scheduling; using UmbracoExamine.DataServices; @@ -18,6 +24,72 @@ namespace Umbraco.Tests.Plugins public class TypeHelperTests { + [Test] + public void Is_Generic_Assignable() + { + var type1 = typeof (DynamicPublishedContentList); + var type2 = typeof (IEnumerable); + var type3 = typeof(IQueryable); + var type4 = typeof(List); + var type5 = typeof(IEnumerable<>); + + Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(type2, type1)); + Assert.IsFalse(TypeHelper.IsTypeAssignableFrom(type3, type1)); + Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(type2, type4)); + + //Will always fail which is correct, you cannot 'assign' IEnumerable simply to IEnumerable<> + //Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(type5, type2)); + + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(type5, type2)); + } + + [Test] + public void Is_Assignable_To_Generic_Type() + { + //modified from: https://gist.github.com/klmr/4174727 + //using a version modified from: http://stackoverflow.com/a/1075059/1968 + + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Base<>), typeof(Derived))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable<>), typeof(List))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Derived<>), typeof(Derived))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Base<>), typeof(Derived2))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IBase<>), typeof(DerivedI))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IBase<>), typeof(Derived2))); + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Nullable<>), typeof(int?))); + + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Object), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(List<>), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable<>), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Base), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable), typeof(List))); + Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Nullable<>), typeof(int))); + + //This get's the "Type" from the Count extension method on IEnumerable, however the type IEnumerable isn't + // IEnumerable<> and it is not a generic definition, this attempts to explain that: + // http://blogs.msdn.com/b/haibo_luo/archive/2006/02/17/534480.aspx + + var genericEnumerableNonGenericDefinition = typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Single(x => x.Name == "Count" && x.GetParameters().Count() == 1) + .GetParameters() + .Single() + .ParameterType; + + Assert.IsTrue(TypeHelper.IsAssignableToGenericType(genericEnumerableNonGenericDefinition, typeof(List))); + + } + + class Base { } + + interface IBase { } + + interface IDerived : IBase { } + + class Derived : Base, IBase { } + + class Derived2 : Derived { } + + class DerivedI : IDerived { } + [Test] public void Is_Static_Class() { From 9cdea752e8ff571ded9957580e0ac5b4782d49f0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 10:24:09 +1100 Subject: [PATCH 2/3] Updates ExtensionMethodFinder to use new TypeFinder methods, all tests are passing now. Moved Stephen's MatchType methods to TypeHelper and moved those tests to TypeHelperTests. Now, need to compare notes with Stephen and add more tests to validate/invalidate the aproaches taken to see what might not be supported between either approaches and should probably look into benchmarks too. --- .../Dynamics/ExtensionMethodFinder.cs | 2 +- .../Factories/UmbracoEntityFactory.cs | 2 +- src/Umbraco.Core/TypeExtensions.cs | 156 ++++-------- src/Umbraco.Core/TypeHelper.cs | 224 +++++++++++------- .../ExtensionMethodFinderTests.cs | 45 +--- src/Umbraco.Tests/Plugins/TypeHelperTests.cs | 76 ++++-- 6 files changed, 255 insertions(+), 250 deletions(-) diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index fd00c99579..6174858c72 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -105,7 +105,7 @@ namespace Umbraco.Core.Dynamics .Select(x => new { methodInfo = x.methodInfo, - genericMethodResult = TypeHelper.IsAssignableToGenericType(x.firstParamType, thisType) + genericMethodResult = TypeHelper.IsAssignableFromGeneric(x.firstParamType, thisType) }) .Where(x => x.genericMethodResult.Success) .Select(x => new ExtensionMethodSearchResult(x.methodInfo, x.genericMethodResult.Result)) diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index f33ae1af2f..683fc1e185 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Persistence.Factories { internal void AddAdditionalData(UmbracoEntity entity, IDictionary originalEntityProperties) { - var entityProps = TypeHelper.GetPublicProperties(typeof(IUmbracoEntity)).Select(x => x.Name).ToArray(); + var entityProps = typeof(IUmbracoEntity).GetPublicProperties().Select(x => x.Name).ToArray(); //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data foreach (var k in originalEntityProperties.Keys diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index 9c015adad4..c7a6945043 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -131,20 +131,7 @@ namespace Umbraco.Core return true; } - - // that method is broken (will return duplicates) and useless (GetInterfaces already does the job) - //public static IEnumerable AllInterfaces(this Type target) - //{ - // foreach (var IF in target.GetInterfaces()) - // { - // yield return IF; - // foreach (var childIF in IF.AllInterfaces()) - // { - // yield return childIF; - // } - // } - //} - + public static IEnumerable GetBaseTypes(this Type type, bool andSelf) { if (andSelf) @@ -308,6 +295,53 @@ namespace Umbraco.Core | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); } + /// + /// Returns all public properties including inherited properties even for interfaces + /// + /// + /// + /// + /// taken from http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy + /// + public static PropertyInfo[] GetPublicProperties(this Type type) + { + if (type.IsInterface) + { + var propertyInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) + { + var subType = queue.Dequeue(); + foreach (var subInterface in subType.GetInterfaces()) + { + if (considered.Contains(subInterface)) continue; + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + var typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance); + + var newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); + } + + return propertyInfos.ToArray(); + } + + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.Instance); + } + /// /// Determines whether the specified actual type is type. /// @@ -389,100 +423,6 @@ namespace Umbraco.Core } - #region Match Type - private static void ReduceGenericParameterCandidateTypes(ICollection allStuff, Type type) - { - var at1 = new List(); - var t = type; - while (t != null) - { - at1.Add(t); - t = t.BaseType; - } - var r = allStuff.Where(x => x.IsClass && at1.Contains(x) == false).ToArray(); - foreach (var x in r) allStuff.Remove(x); - var ai1 = type.GetInterfaces(); - if (type.IsInterface) ai1 = ai1.Union(new[] { type }).ToArray(); - r = allStuff.Where(x => x.IsInterface && ai1.Contains(x) == false).ToArray(); - foreach (var x in r) allStuff.Remove(x); - } - - private static bool MatchGeneric(Type inst, Type type, IDictionary> bindings) - { - if (inst.IsGenericType == false) return false; - - var instd = inst.GetGenericTypeDefinition(); - var typed = type.GetGenericTypeDefinition(); - - if (instd != typed) return false; - - var insta = inst.GetGenericArguments(); - var typea = type.GetGenericArguments(); - - if (insta.Length != typea.Length) return false; - - // but... there is no ZipWhile, and we have arrays anyway - //var x = insta.Zip(typea, (instax, typeax) => { ... }); - - for (var i = 0; i < insta.Length; i++) - if (MatchType(insta[i], typea[i], bindings) == false) - return false; - - return true; - } - - private static IEnumerable GetGenericParameterCandidateTypes(Type type) - { - yield return type; - var t = type.BaseType; - while (t != null) - { - yield return t; - t = t.BaseType; - } - foreach (var i in type.GetInterfaces()) - yield return i; - } - - public static bool MatchType(this Type inst, Type type) - { - return MatchType(inst, type, new Dictionary>()); - } - - internal static bool MatchType(this Type inst, Type type, IDictionary> bindings) - { - if (type.IsGenericType) - { - if (MatchGeneric(inst, type, bindings)) return true; - var t = inst.BaseType; - while (t != null) - { - if (MatchGeneric(t, type, bindings)) return true; - t = t.BaseType; - } - return inst.GetInterfaces().Any(i => MatchGeneric(i, type, bindings)); - } - - if (type.IsGenericParameter) - { - if (bindings.ContainsKey(type.Name)) - { - ReduceGenericParameterCandidateTypes(bindings[type.Name], inst); - return bindings[type.Name].Count > 0; - } - - bindings[type.Name] = new List(GetGenericParameterCandidateTypes(inst)); - return true; - } - - if (inst == type) return true; - if (type.IsClass && inst.IsClass && inst.IsSubclassOf(type)) return true; - if (type.IsInterface && inst.GetInterfaces().Contains(type)) return true; - - return false; - } - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index 03e5769c1c..63dd60fba9 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -134,80 +134,84 @@ namespace Umbraco.Core /// /// Checks if the generic type passed in can be assigned from the given type /// - /// - /// + /// + /// /// /// Returns an Attempt{Type} which if true will include the actual type that matched the genericType /// being compared. /// /// - /// First we need to check a special case, if the generic type is a generic definition but has not FullName, then + /// First we need to check a special case, if the generic type is a generic definition but has no FullName, then /// we cannot compare it with traditional means because the types will never match. /// Generic types will not have a FullName in these cases: http://blogs.msdn.com/b/haibo_luo/archive/2006/02/17/534480.aspx - /// or apparently when you retrieve a generic method parameter using reflection. + /// or when you retrieve a generic method parameter using reflection, for example, typeof(IEnumerable{}) is not IEnumerable{T} since + /// when reflected 'T' is actually something. /// /// This is using a version modified from: http://stackoverflow.com/a/1075059/1968 /// - public static Attempt IsAssignableToGenericType(Type genericType, Type givenType) + public static Attempt IsAssignableFromGeneric(Type contract, Type implementation) { - if (genericType.IsGenericTypeDefinition == false && genericType.FullName.IsNullOrWhiteSpace()) + if (contract.IsGenericTypeDefinition == false && contract.FullName.IsNullOrWhiteSpace()) { - return IsAssignableToReflectedGenericType(genericType, givenType); + return IsTypeAssignableFromReflectedGeneric(contract, implementation); } - var genericTypeDef = givenType.IsGenericType ? givenType.GetGenericTypeDefinition() : null; + var genericTypeDef = implementation.IsGenericType ? implementation.GetGenericTypeDefinition() : null; - if (genericTypeDef != null && genericTypeDef == genericType) - return Attempt.Succeed(givenType); + if (genericTypeDef != null && genericTypeDef == contract) + return Attempt.Succeed(implementation); - var its = givenType.GetInterfaces(); + var its = implementation.GetInterfaces(); foreach (var it in its) { genericTypeDef = it.IsGenericType ? it.GetGenericTypeDefinition() : null; - if (genericTypeDef != null && genericTypeDef == genericType) + if (genericTypeDef != null && genericTypeDef == contract) return Attempt.Succeed(it); } - var baseType = givenType.BaseType; + var baseType = implementation.BaseType; return baseType != null - ? IsAssignableToGenericType(genericType, baseType) + ? IsAssignableFromGeneric(contract, baseType) : Attempt.Fail(); } /// /// This is used in IsAssignableToGenericType /// - /// - /// + /// The generic type contract + /// /// /// Returns an Attempt{Type} which if true will include the actual type that matched the genericType /// being compared. /// - private static Attempt IsAssignableToReflectedGenericType(Type genericType, Type givenType) + /// + /// See remarks in method IsAssignableFromGeneric + /// + private static Attempt IsTypeAssignableFromReflectedGeneric(Type contract, Type implementation) { - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition().Name == genericType.Name && givenType.GenericTypeArguments.Length == genericType.GenericTypeArguments.Length) - return Attempt.Succeed(givenType); + if (implementation.IsGenericType && implementation.GetGenericTypeDefinition().Name == contract.Name && implementation.GenericTypeArguments.Length == contract.GenericTypeArguments.Length) + return Attempt.Succeed(implementation); - var its = givenType.GetInterfaces(); + var its = implementation.GetInterfaces(); foreach (var it in its) { - if (it.IsGenericType && it.GetGenericTypeDefinition().Name == genericType.Name && givenType.GenericTypeArguments.Length == genericType.GenericTypeArguments.Length) + if (it.IsGenericType && it.GetGenericTypeDefinition().Name == contract.Name && implementation.GenericTypeArguments.Length == contract.GenericTypeArguments.Length) { return Attempt.Succeed(it); } } - var baseType = givenType.BaseType; + var baseType = implementation.BaseType; return baseType != null - ? IsAssignableToReflectedGenericType(genericType, baseType) + ? IsTypeAssignableFromReflectedGeneric(contract, baseType) : Attempt.Fail(); } /// - /// Determines whether the type is assignable from the specified implementation , + /// Determines whether the type is assignable from the specified implementation, /// and caches the result across the application using a . /// /// The type of the contract. @@ -231,8 +235,20 @@ namespace Umbraco.Core return IsTypeAssignableFrom(typeof(TContract), implementation); } + /// + /// Determines whether the object instance is assignable from the specified implementation , + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + public static bool IsTypeAssignableFrom(object implementation) + { + if (implementation == null) throw new ArgumentNullException("implementation"); + return IsTypeAssignableFrom(implementation.GetType()); + } + /// - /// A cached method to determine whether represents a value type. + /// A method to determine whether represents a value type. /// /// The implementation. public static bool IsValueType(Type implementation) @@ -241,7 +257,7 @@ namespace Umbraco.Core } /// - /// A cached method to determine whether is an implied value type (, or a string). + /// A method to determine whether is an implied value type (, or a string). /// /// The implementation. public static bool IsImplicitValueType(Type implementation) @@ -249,14 +265,8 @@ namespace Umbraco.Core return IsValueType(implementation) || implementation.IsEnum || implementation == typeof (string); } - public static bool IsTypeAssignableFrom(object implementation) - { - if (implementation == null) throw new ArgumentNullException("implementation"); - return IsTypeAssignableFrom(implementation.GetType()); - } - /// - /// Returns a PropertyInfo from a type + /// Returns (and caches) a PropertyInfo from a type /// /// /// @@ -278,54 +288,7 @@ namespace Umbraco.Core return x.Name == name; return x.Name.InvariantEquals(name); }); - } - - /// - /// Returns all public properties including inherited properties even for interfaces - /// - /// - /// - /// - /// taken from http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy - /// - public static PropertyInfo[] GetPublicProperties(Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetInterfaces()) - { - if (considered.Contains(subInterface)) continue; - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - var typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.Instance); - - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos.ToArray(); - } - - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.Instance); - } + } /// /// Gets (and caches) discoverable in the current for a given . @@ -361,5 +324,104 @@ namespace Umbraco.Core && (includeIndexed || !y.GetIndexParameters().Any())) .ToArray()); } + + + #region Match Type + + //TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric + + private static void ReduceGenericParameterCandidateTypes(ICollection allStuff, Type type) + { + var at1 = new List(); + var t = type; + while (t != null) + { + at1.Add(t); + t = t.BaseType; + } + var r = allStuff.Where(x => x.IsClass && at1.Contains(x) == false).ToArray(); + foreach (var x in r) allStuff.Remove(x); + var ai1 = type.GetInterfaces(); + if (type.IsInterface) ai1 = ai1.Union(new[] { type }).ToArray(); + r = allStuff.Where(x => x.IsInterface && ai1.Contains(x) == false).ToArray(); + foreach (var x in r) allStuff.Remove(x); + } + + private static bool MatchGeneric(Type inst, Type type, IDictionary> bindings) + { + if (inst.IsGenericType == false) return false; + + var instd = inst.GetGenericTypeDefinition(); + var typed = type.GetGenericTypeDefinition(); + + if (instd != typed) return false; + + var insta = inst.GetGenericArguments(); + var typea = type.GetGenericArguments(); + + if (insta.Length != typea.Length) return false; + + // but... there is no ZipWhile, and we have arrays anyway + //var x = insta.Zip(typea, (instax, typeax) => { ... }); + + for (var i = 0; i < insta.Length; i++) + if (MatchType(insta[i], typea[i], bindings) == false) + return false; + + return true; + } + + private static IEnumerable GetGenericParameterCandidateTypes(Type type) + { + yield return type; + var t = type.BaseType; + while (t != null) + { + yield return t; + t = t.BaseType; + } + foreach (var i in type.GetInterfaces()) + yield return i; + } + + public static bool MatchType(Type inst, Type type) + { + return MatchType(inst, type, new Dictionary>()); + } + + internal static bool MatchType(Type inst, Type type, IDictionary> bindings) + { + if (type.IsGenericType) + { + if (MatchGeneric(inst, type, bindings)) return true; + var t = inst.BaseType; + while (t != null) + { + if (MatchGeneric(t, type, bindings)) return true; + t = t.BaseType; + } + return inst.GetInterfaces().Any(i => MatchGeneric(i, type, bindings)); + } + + if (type.IsGenericParameter) + { + if (bindings.ContainsKey(type.Name)) + { + ReduceGenericParameterCandidateTypes(bindings[type.Name], inst); + return bindings[type.Name].Count > 0; + } + + bindings[type.Name] = new List(GetGenericParameterCandidateTypes(inst)); + return true; + } + + if (inst == type) return true; + if (type.IsClass && inst.IsClass && inst.IsSubclassOf(type)) return true; + if (type.IsInterface && inst.GetInterfaces().Contains(type)) return true; + + return false; + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs index cc4dd5b5a7..aab71e364a 100644 --- a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -202,50 +202,7 @@ namespace Umbraco.Tests.DynamicsAndReflection Assert.AreEqual("T", t3.GetGenericArguments()[0].Name); } - [Test] - public void MatchTypesTest() - { - var bindings = new Dictionary>(); - Assert.IsTrue(typeof(int).MatchType(typeof(int), bindings)); - Assert.AreEqual(0, bindings.Count); - - bindings = new Dictionary>(); - Assert.IsFalse(typeof(int).MatchType(typeof(string), bindings)); - Assert.AreEqual(0, bindings.Count); - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(List).MatchType(typeof(System.Collections.IEnumerable), bindings)); - Assert.AreEqual(0, bindings.Count); - - var m = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod7"); - var t1 = m.GetParameters()[0].ParameterType; // List - var t2 = m.GetParameters()[0].ParameterType.GetGenericArguments()[0]; // - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(int).MatchType(t2, bindings)); - Assert.AreEqual(1, bindings.Count); - Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(IList).MatchType(t1, bindings)); - Assert.AreEqual(1, bindings.Count); - Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(List).MatchType(typeof(IList), bindings)); - Assert.AreEqual(0, bindings.Count); - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(List).MatchType(t1, bindings)); - Assert.AreEqual(1, bindings.Count); - Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); - - bindings = new Dictionary>(); - Assert.IsTrue(typeof(Dictionary).MatchType(typeof(IDictionary<,>), bindings)); - Assert.AreEqual(2, bindings.Count); - Assert.AreEqual(typeof(int), bindings["TKey"].FirstOrDefault()); - Assert.AreEqual(typeof(string), bindings["TValue"].FirstOrDefault()); - } + [Test] public void Find_Generic_Enumerable_Method() diff --git a/src/Umbraco.Tests/Plugins/TypeHelperTests.cs b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs index d66c06efda..9741e9cb2e 100644 --- a/src/Umbraco.Tests/Plugins/TypeHelperTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs @@ -10,6 +10,7 @@ using System.Reflection; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Tests.DynamicsAndReflection; using Umbraco.Web.Cache; using Umbraco.Web.Models; using Umbraco.Web.Scheduling; @@ -40,7 +41,7 @@ namespace Umbraco.Tests.Plugins //Will always fail which is correct, you cannot 'assign' IEnumerable simply to IEnumerable<> //Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(type5, type2)); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(type5, type2)); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(type5, type2)); } [Test] @@ -49,20 +50,20 @@ namespace Umbraco.Tests.Plugins //modified from: https://gist.github.com/klmr/4174727 //using a version modified from: http://stackoverflow.com/a/1075059/1968 - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Base<>), typeof(Derived))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable<>), typeof(List))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Derived<>), typeof(Derived))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Base<>), typeof(Derived2))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IBase<>), typeof(DerivedI))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(IBase<>), typeof(Derived2))); - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(typeof(Nullable<>), typeof(int?))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(Base<>), typeof(Derived))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(IEnumerable<>), typeof(List))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(Derived<>), typeof(Derived))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(Base<>), typeof(Derived2))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(IBase<>), typeof(DerivedI))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(IBase<>), typeof(Derived2))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(typeof(Nullable<>), typeof(int?))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Object), typeof(Derived))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(List<>), typeof(Derived))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable<>), typeof(Derived))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Base), typeof(Derived))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(IEnumerable), typeof(List))); - Assert.IsFalse(TypeHelper.IsAssignableToGenericType(typeof(Nullable<>), typeof(int))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(Object), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(List<>), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(IEnumerable<>), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(Base), typeof(Derived))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(IEnumerable), typeof(List))); + Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(typeof(Nullable<>), typeof(int))); //This get's the "Type" from the Count extension method on IEnumerable, however the type IEnumerable isn't // IEnumerable<> and it is not a generic definition, this attempts to explain that: @@ -74,7 +75,7 @@ namespace Umbraco.Tests.Plugins .Single() .ParameterType; - Assert.IsTrue(TypeHelper.IsAssignableToGenericType(genericEnumerableNonGenericDefinition, typeof(List))); + Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(genericEnumerableNonGenericDefinition, typeof(List))); } @@ -140,5 +141,50 @@ namespace Umbraco.Tests.Plugins } + [Test] + public void MatchTypesTest() + { + var bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(int), typeof(int), bindings)); + Assert.AreEqual(0, bindings.Count); + + bindings = new Dictionary>(); + Assert.IsFalse(TypeHelper.MatchType(typeof(int), typeof(string), bindings)); + Assert.AreEqual(0, bindings.Count); + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(List), typeof(System.Collections.IEnumerable), bindings)); + Assert.AreEqual(0, bindings.Count); + + var m = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod7"); + var t1 = m.GetParameters()[0].ParameterType; // List + var t2 = m.GetParameters()[0].ParameterType.GetGenericArguments()[0]; // + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(int), t2, bindings)); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(IList), t1, bindings)); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(List), typeof(IList), bindings)); + Assert.AreEqual(0, bindings.Count); + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(List), t1, bindings)); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault()); + + bindings = new Dictionary>(); + Assert.IsTrue(TypeHelper.MatchType(typeof(Dictionary), typeof(IDictionary<,>), bindings)); + Assert.AreEqual(2, bindings.Count); + Assert.AreEqual(typeof(int), bindings["TKey"].FirstOrDefault()); + Assert.AreEqual(typeof(string), bindings["TValue"].FirstOrDefault()); + } + } } \ No newline at end of file From a7c134a3758e1f4c4ceea86b5fc0656dee27b684 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 12:11:10 +1100 Subject: [PATCH 3/3] fixes last failing test --- src/Umbraco.Tests/Services/LocalizedTextServiceTests.cs | 4 ++-- src/Umbraco.Web/Models/DynamicPublishedContent.cs | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Tests/Services/LocalizedTextServiceTests.cs b/src/Umbraco.Tests/Services/LocalizedTextServiceTests.cs index 6e4f3e8c12..c57010d29f 100644 --- a/src/Umbraco.Tests/Services/LocalizedTextServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizedTextServiceTests.cs @@ -364,7 +364,7 @@ namespace Umbraco.Tests.Services } [Test] - public void Using_XDocument_Throws_When_No_Culture_Found() + public void Using_XDocument_Returns_Default_Text_When_No_Culture_Found() { var culture = CultureInfo.GetCultureInfo("en-US"); var txtService = new LocalizedTextService( @@ -378,7 +378,7 @@ namespace Umbraco.Tests.Services } }); - Assert.Throws(() => txtService.Localize("testArea/testKey", CultureInfo.GetCultureInfo("en-AU"))); + Assert.AreEqual("[testKey]", txtService.Localize("testArea/testKey", CultureInfo.GetCultureInfo("en-AU"))); } } } diff --git a/src/Umbraco.Web/Models/DynamicPublishedContent.cs b/src/Umbraco.Web/Models/DynamicPublishedContent.cs index 83874623b8..26a5517039 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContent.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContent.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; using System.Linq; -using System.Runtime.CompilerServices; using System.Web; using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; @@ -16,8 +15,6 @@ using Umbraco.Core.Models; using Umbraco.Core; using System.Reflection; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Strings; -using ContentType = umbraco.cms.businesslogic.ContentType; namespace Umbraco.Web.Models {