From 1b374751442723e29af0c457a30ec2c2a26b7069 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 9 Mar 2016 15:55:41 +0100 Subject: [PATCH] U4-6438 - fix dynamic extension methods when type loading goes bad --- .../Dynamics/ExtensionMethodFinder.cs | 36 +++++--- src/Umbraco.Core/TypeFinder.cs | 87 +++++++++++++------ 2 files changed, 85 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index 4430d1d74e..9772aaf5bd 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Linq.Expressions; +using System.Text; using System.Web.Services.Description; using Umbraco.Core.Cache; +using Umbraco.Core.Logging; namespace Umbraco.Core.Dynamics { @@ -20,6 +22,20 @@ namespace Umbraco.Core.Dynamics /// private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); + private static IEnumerable GetTypes(Assembly a) + { + try + { + return TypeFinder.GetTypesWithFormattedException(a); + } + catch (ReflectionTypeLoadException ex) + { + // is this going to flood the log? + LogHelper.Error(typeof (ExtensionMethodFinder), "Failed to get types.", ex); + return Enumerable.Empty(); + } + } + /// /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! /// @@ -36,7 +52,7 @@ namespace Umbraco.Core.Dynamics // assemblies that contain extension methods .Where(a => a.IsDefined(typeof (ExtensionAttribute), false)) // types that contain extension methods - .SelectMany(a => a.GetTypes() + .SelectMany(a => GetTypes(a) .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) @@ -45,9 +61,9 @@ namespace Umbraco.Core.Dynamics .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), + timeout: TimeSpan.FromMinutes(5), //each time this is accessed it will be for 5 minutes longer isSliding:true); @@ -57,7 +73,7 @@ namespace Umbraco.Core.Dynamics /// 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 + /// 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. /// @@ -78,7 +94,7 @@ namespace Umbraco.Core.Dynamics { var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // filter by name + // filter by name var filtr1 = candidates.Where(m => m.Name == name); // filter by args count @@ -102,7 +118,7 @@ namespace Umbraco.Core.Dynamics return filtr3.ToArray(); }); - + } private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) @@ -123,12 +139,12 @@ namespace Umbraco.Core.Dynamics types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1) }); - //This type comparer will check + //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 + //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, + 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()); @@ -159,7 +175,7 @@ namespace Umbraco.Core.Dynamics .ToArray(); var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); - + return DetermineMethodFromParams(methods, genericType, args); } } diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index e99a491939..f970cf225b 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core /// /// A utility class to find all classes of a certain type by reflection in the current bin folder - /// of the web application. + /// of the web application. /// public static class TypeFinder { @@ -34,7 +34,7 @@ namespace Umbraco.Core /// This is a modified version of: http://www.dominicpettifer.co.uk/Blog/44/how-to-get-a-reference-to-all-assemblies-in-the--bin-folder /// /// - /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been + /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been /// loaded in the CLR, not all assemblies. /// See these threads: /// http://issues.umbraco.org/issue/U5-198 @@ -136,7 +136,7 @@ namespace Umbraco.Core }); /// - /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan + /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan /// and exluding the ones passed in and excluding the exclusion list filter, the results of this are /// cached for perforance reasons. /// @@ -190,6 +190,7 @@ namespace Umbraco.Core /// /// /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match + /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" /// internal static readonly string[] KnownAssemblyExclusionFilter = new[] { @@ -214,7 +215,7 @@ namespace Umbraco.Core "RouteDebugger,", "SqlCE4Umbraco,", "umbraco.datalayer,", - "umbraco.interfaces,", + "umbraco.interfaces,", //"umbraco.providers,", //"Umbraco.Web.UI,", "umbraco.webservices", @@ -230,9 +231,9 @@ namespace Umbraco.Core "AutoMapper,", "AutoMapper.", "AzureDirectory,", - "itextsharp,", + "itextsharp,", "UrlRewritingNet.", - "HtmlAgilityPack,", + "HtmlAgilityPack,", "MiniProfiler,", "Moq,", "nunit.framework,", @@ -371,7 +372,7 @@ namespace Umbraco.Core var assemblyList = assemblies.ToArray(); - //find all assembly references that are referencing the attribute type's assembly since we + //find all assembly references that are referencing the attribute type's assembly since we //should only be scanning those assemblies because any other assembly will definitely not //contain a class that has this attribute. var referencedAssemblies = TypeHelper.GetReferencedAssemblies(attributeType, assemblyList); @@ -418,7 +419,7 @@ namespace Umbraco.Core foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)) { - //So that we are not scanning too much, we need to group the sub types: + //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class // * We should not search for sub types if the type is static since you cannot inherit from them. @@ -489,7 +490,7 @@ namespace Umbraco.Core /// /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly - /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type + /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type /// deriving from the base type. /// /// @@ -513,7 +514,7 @@ namespace Umbraco.Core var assemblyList = assemblies.ToArray(); - //find all assembly references that are referencing the current type's assembly since we + //find all assembly references that are referencing the current type's assembly since we //should only be scanning those assemblies because any other assembly will definitely not //contain sub type's of the one we're currently looking for var referencedAssemblies = TypeHelper.GetReferencedAssemblies(assignTypeFrom, assemblyList); @@ -560,7 +561,7 @@ namespace Umbraco.Core foreach (var subTypesInAssembly in allSubTypes.GroupBy(x => x.Assembly)) { - //So that we are not scanning too much, we need to group the sub types: + //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. // * We should also not search for sub types if the type is sealed since you cannot inherit from a sealed class // * We should not search for sub types if the type is static since you cannot inherit from them. @@ -602,39 +603,69 @@ namespace Umbraco.Core return foundAssignableTypes; } - private static IEnumerable GetTypesWithFormattedException(Assembly a) + internal static IEnumerable GetTypesWithFormattedException(Assembly a) { //if the assembly is dynamic, do not try to scan it if (a.IsDynamic) return Enumerable.Empty(); + var getAll = a.GetCustomAttribute() == null; + try { //we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types //only its exported types, otherwise we'll get exceptions. - if (a.GetCustomAttribute() == null) - { - return a.GetTypes(); - } - else - { - return a.GetExportedTypes(); - } + return getAll ? a.GetTypes() : a.GetExportedTypes(); } - catch (ReflectionTypeLoadException ex) + catch (TypeLoadException ex) // GetExportedTypes *can* throw TypeLoadException! { var sb = new StringBuilder(); - sb.AppendLine("Could not load types from assembly " + a.FullName + ", errors:"); - foreach (var loaderException in ex.LoaderExceptions.WhereNotNull()) - { - sb.AppendLine("Exception: " + loaderException); - } - throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); + AppendCouldNotLoad(sb, a, getAll); + AppendLoaderException(sb, ex); + + // rethrow as ReflectionTypeLoadException (for consistency) with new message + throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); + } + catch (ReflectionTypeLoadException rex) // GetTypes throws ReflectionTypeLoadException + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) + AppendLoaderException(sb, loaderException); + + // rethrow with new message + throw new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); } } + private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) + { + sb.Append("Could not load "); + sb.Append(getAll ? "all" : "exported"); + sb.Append(" types from \""); + sb.Append(a.FullName); + sb.AppendLine("\" due to LoaderExceptions, skipping:"); + } + + private static void AppendLoaderException(StringBuilder sb, Exception loaderException) + { + sb.Append(". "); + sb.Append(loaderException.GetType().FullName); + + var tloadex = loaderException as TypeLoadException; + if (tloadex != null) + { + sb.Append(" on "); + sb.Append(tloadex.TypeName); + } + + sb.Append(": "); + sb.Append(loaderException.Message); + sb.AppendLine(); + } + #endregion - + public static Type GetTypeByName(string typeName) {