diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index c477b8d49c..883bf2176f 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -143,7 +143,8 @@ namespace Umbraco.Core { try { - var foundAssembly = safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); + var foundAssembly = + safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); if (foundAssembly != null) { binFolderAssemblies.Add(foundAssembly); @@ -255,11 +256,17 @@ namespace Umbraco.Core return FindClassesOfTypeWithAttribute(assemblies, true); } - public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) + public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies, + bool onlyConcreteClasses) where TAttribute : Attribute { if (assemblies == null) throw new ArgumentNullException("assemblies"); + // a assembly cant contain types that are assignable to a type it doesn't reference + assemblies = RemoveAssembliesThatDontReferenceAssemblyOfType(typeof (T), assemblies); + // a assembly cant contain types with a attribute it doesn't reference + assemblies = RemoveAssembliesThatDontReferenceAssemblyOfType(typeof (TAttribute), assemblies); + var l = new List(); foreach(var a in assemblies) { @@ -330,23 +337,57 @@ namespace Umbraco.Core /// The assemblies. /// if set to true only concrete classes. /// - public static IEnumerable FindClassesWithAttribute(Type type, IEnumerable assemblies, bool onlyConcreteClasses) + public static IEnumerable FindClassesWithAttribute(Type type, IEnumerable assemblies, + bool onlyConcreteClasses) { if (assemblies == null) throw new ArgumentNullException("assemblies"); if (!TypeHelper.IsTypeAssignableFrom(type)) throw new ArgumentException("The type specified: " + type + " is not an Attribute type"); + // a assembly cant contain types with a attribute it doesn't reference + assemblies = RemoveAssembliesThatDontReferenceAssemblyOfType(type, assemblies); var l = new List(); foreach (var a in assemblies) { var types = from t in GetTypesWithFormattedException(a) - where !t.IsInterface && t.GetCustomAttributes(type, false).Any() && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + where + !t.IsInterface && t.GetCustomAttributes(type, false).Any() && + (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) select t; l.AddRange(types); } return l; } + /// + /// Removes assemblies that doesn't reference the assembly of the type we are looking for. + /// + /// + /// + /// + private static IEnumerable RemoveAssembliesThatDontReferenceAssemblyOfType(Type type, IEnumerable assemblies) + { + // Avoid scanning assembly if it doesn't contain a reference to the assembly containing the type we are looking for + // to the assembly containing the attribute we are looking for + var assemblyNameOfType = type.Assembly.GetName().Name; + + return assemblies + .Where(assembly => assembly == type.Assembly + || HasReferenceToAssemblyWithName(assembly, assemblyNameOfType)).ToList(); + } + /// + /// checks if the assembly has a reference with the same name as the expected assembly name. + /// + /// + /// + /// + private static bool HasReferenceToAssemblyWithName(Assembly assembly, string expectedAssemblyName) + { + return assembly + .GetReferencedAssemblies() + .Select(a => a.Name) + .Contains(expectedAssemblyName, StringComparer.Ordinal); + } /// /// Finds the classes with attribute. @@ -388,12 +429,17 @@ namespace Umbraco.Core private static IEnumerable GetTypes(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses) { + // a assembly cant contain types that are assignable to a type it doesn't reference + assemblies = RemoveAssembliesThatDontReferenceAssemblyOfType(assignTypeFrom, assemblies); + var l = new List(); foreach (var a in assemblies) { - var types = from t in GetTypesWithFormattedException(a) - where !t.IsInterface && assignTypeFrom.IsAssignableFrom(t) && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) - select t; + var types = from t in GetTypesWithFormattedException(a) + where + !t.IsInterface && assignTypeFrom.IsAssignableFrom(t) && + (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; l.AddRange(types); } return l; @@ -411,7 +457,7 @@ namespace Umbraco.Core sb.AppendLine("Could not load types from assembly " + a.FullName + ", errors:"); foreach (var loaderException in ex.LoaderExceptions.WhereNotNull()) { - sb.AppendLine("Exception: " + loaderException.ToString()); + sb.AppendLine("Exception: " + loaderException); } throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); } diff --git a/src/Umbraco.Tests/TypeFinderTests.cs b/src/Umbraco.Tests/TypeFinderTests.cs index 50edb8ef2f..1e0fe16e4c 100644 --- a/src/Umbraco.Tests/TypeFinderTests.cs +++ b/src/Umbraco.Tests/TypeFinderTests.cs @@ -1,19 +1,31 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Security; +using System.Text; +using System.Threading; +using System.Web; +using System.Web.Compilation; using NUnit.Framework; using SqlCE4Umbraco; using Umbraco.Core; +using Umbraco.Core.IO; using Umbraco.Tests; using Umbraco.Tests.PartialTrust; using Umbraco.Tests.TestHelpers; +using Umbraco.Web.BaseRest; using umbraco; using umbraco.DataLayer; using umbraco.MacroEngines; using umbraco.businesslogic; using umbraco.cms.businesslogic; +using umbraco.editorControls.tags; +using umbraco.interfaces; using umbraco.uicontrols; namespace Umbraco.Tests @@ -50,45 +62,561 @@ namespace Umbraco.Tests typeof(System.Web.Mvc.ActionResult).Assembly, typeof(TypeFinder).Assembly, typeof(ISqlHelper).Assembly, - typeof(ICultureDictionary).Assembly + typeof(ICultureDictionary).Assembly, + typeof(Tag).Assembly }; } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class MyTestAttribute : Attribute - { - - } - - public abstract class TestEditor - { - - } - - [MyTestAttribute] - public class BenchmarkTestEditor : TestEditor - { - - } - - [MyTestAttribute] - public class MyOtherTestEditor : TestEditor - { - - } [Test] - public void Get_Type_With_Attribute() + public void Find_Class_Of_Type_With_Attribute() { var typesFound = TypeFinder.FindClassesOfTypeWithAttribute(_assemblies); - Assert.AreEqual(2, typesFound.Count()); - } - - + + [Test] + public void Find_Classes_Of_Type() + { + var typesFound = TypeFinder.FindClassesOfType(_assemblies); + Assert.AreEqual(2, typesFound.Count()); + } + + [Test] + public void Find_Classes_With_Attribute() + { + var typesFound = TypeFinder.FindClassesWithAttribute(_assemblies); + Assert.AreEqual(1, typesFound.Count()); + } + + [Ignore] + [Test] + public void Benchmark_Original_Finder() + { + using (DisposableTimer.TraceDuration("Starting test", "Finished test")) + { + using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesOfType(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesWithAttribute(_assemblies).Count(), 0); + } + } + } + + } + + [Ignore] + [Test] + public void Benchmark_New_Finder() + { + using (DisposableTimer.TraceDuration("Starting test", "Finished test")) + { + using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesOfType(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesWithAttribute(_assemblies).Count(), 0); + } + } + } + + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class MyTestAttribute : Attribute + { + + } + + public abstract class TestEditor + { + + } + + [MyTestAttribute] + public class BenchmarkTestEditor : TestEditor + { + + } + + [MyTestAttribute] + public class MyOtherTestEditor : TestEditor + { + + } + + //USED FOR THE ABOVE TESTS + // see this issue for details: http://issues.umbraco.org/issue/U4-1187 + internal static class TypeFinderOriginal + { + + private static readonly ConcurrentBag LocalFilteredAssemblyCache = new ConcurrentBag(); + private static readonly ReaderWriterLockSlim LocalFilteredAssemblyCacheLocker = new ReaderWriterLockSlim(); + private static ReadOnlyCollection _allAssemblies = null; + private static ReadOnlyCollection _binFolderAssemblies = null; + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + + /// + /// lazily load a reference to all assemblies and only local assemblies. + /// 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 + /// loaded in the CLR, not all assemblies. + /// See these threads: + /// http://issues.umbraco.org/issue/U5-198 + /// http://stackoverflow.com/questions/3552223/asp-net-appdomain-currentdomain-getassemblies-assemblies-missing-after-app + /// http://stackoverflow.com/questions/2477787/difference-between-appdomain-getassemblies-and-buildmanager-getreferencedassembl + /// + internal static IEnumerable GetAllAssemblies() + { + if (_allAssemblies == null) + { + using (new WriteLock(Locker)) + { + List assemblies = null; + try + { + var isHosted = HttpContext.Current != null; + + try + { + if (isHosted) + { + assemblies = new List(BuildManager.GetReferencedAssemblies().Cast()); + } + } + catch (InvalidOperationException e) + { + if (!(e.InnerException is SecurityException)) + throw; + } + + + if (assemblies == null) + { + //NOTE: we cannot use AppDomain.CurrentDomain.GetAssemblies() because this only returns assemblies that have + // already been loaded in to the app domain, instead we will look directly into the bin folder and load each one. + var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; + var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + assemblies = new List(); + foreach (var a in binAssemblyFiles) + { + try + { + var assName = AssemblyName.GetAssemblyName(a); + var ass = Assembly.Load(assName); + assemblies.Add(ass); + } + catch (Exception e) + { + if (e is SecurityException || e is BadImageFormatException) + { + //swallow these exceptions + } + else + { + throw; + } + } + } + } + + //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. + if (!assemblies.Any()) + { + assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().ToList()); + } + + //here we are trying to get the App_Code assembly + var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported + var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); + //check if the folder exists and if there are any files in it with the supported file extensions + if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) + { + var appCodeAssembly = Assembly.Load("App_Code"); + if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already + assemblies.Add(appCodeAssembly); + } + + //now set the _allAssemblies + _allAssemblies = new ReadOnlyCollection(assemblies); + + } + catch (InvalidOperationException e) + { + if (!(e.InnerException is SecurityException)) + throw; + + _binFolderAssemblies = _allAssemblies; + } + } + } + + return _allAssemblies; + } + + /// + /// Returns only assemblies found in the bin folder that have been loaded into the app domain. + /// + /// + /// + /// This will be used if we implement App_Plugins from Umbraco v5 but currently it is not used. + /// + internal static IEnumerable GetBinAssemblies() + { + + if (_binFolderAssemblies == null) + { + using (new WriteLock(Locker)) + { + var assemblies = GetAssembliesWithKnownExclusions().ToArray(); + var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; + var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + var domainAssemblyNames = binAssemblyFiles.Select(AssemblyName.GetAssemblyName); + var safeDomainAssemblies = new List(); + var binFolderAssemblies = new List(); + + foreach (var a in assemblies) + { + try + { + //do a test to see if its queryable in med trust + var assemblyFile = a.GetAssemblyFile(); + safeDomainAssemblies.Add(a); + } + catch (SecurityException) + { + //we will just ignore this because this will fail + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //an assembly that is ok. + } + } + + foreach (var assemblyName in domainAssemblyNames) + { + try + { + var foundAssembly = safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); + if (foundAssembly != null) + { + binFolderAssemblies.Add(foundAssembly); + } + } + catch (SecurityException) + { + //we will just ignore this because if we are trying to do a call to: + // AssemblyName.ReferenceMatchesDefinition(a.GetName(), assemblyName))) + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //an assembly that is ok. + } + } + + _binFolderAssemblies = new ReadOnlyCollection(binFolderAssemblies); + } + } + return _binFolderAssemblies; + } + + /// + /// 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. + /// + /// + /// + internal static IEnumerable GetAssembliesWithKnownExclusions( + IEnumerable excludeFromResults = null) + { + if (LocalFilteredAssemblyCache.Any()) return LocalFilteredAssemblyCache; + using (new WriteLock(LocalFilteredAssemblyCacheLocker)) + { + var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); + assemblies.ForEach(LocalFilteredAssemblyCache.Add); + } + return LocalFilteredAssemblyCache; + } + + /// + /// Return a list of found local Assemblies and exluding the ones passed in and excluding the exclusion list filter + /// + /// + /// + /// + private static IEnumerable GetFilteredAssemblies( + IEnumerable excludeFromResults = null, + string[] exclusionFilter = null) + { + if (excludeFromResults == null) + excludeFromResults = new List(); + if (exclusionFilter == null) + exclusionFilter = new string[] { }; + + return GetAllAssemblies() + .Where(x => !excludeFromResults.Contains(x) + && !x.GlobalAssemblyCache + && !exclusionFilter.Any(f => x.FullName.StartsWith(f))); + } + + /// + /// this is our assembly filter to filter out known types that def dont contain types we'd like to find or plugins + /// + /// + /// 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 + /// + internal static readonly string[] KnownAssemblyExclusionFilter = new[] + { + "mscorlib,", + "System.", + "Antlr3.", + "Autofac.", + "Autofac,", + "Castle.", + "ClientDependency.", + "DataAnnotationsExtensions.", + "DataAnnotationsExtensions,", + "Dynamic,", + "HtmlDiff,", + "Iesi.Collections,", + "log4net,", + "Microsoft.", + "Newtonsoft.", + "NHibernate.", + "NHibernate,", + "NuGet.", + "RouteDebugger,", + "SqlCE4Umbraco,", + "umbraco.datalayer,", + "umbraco.interfaces,", + "umbraco.providers,", + "Umbraco.Web.UI,", + "umbraco.webservices", + "Lucene.", + "Examine,", + "Examine.", + "ServiceStack.", + "MySql.", + "HtmlAgilityPack.", + "TidyNet.", + "ICSharpCode.", + "CookComputing." + }; + + public static IEnumerable FindClassesOfTypeWithAttribute() + where TAttribute : Attribute + { + return FindClassesOfTypeWithAttribute(GetAssembliesWithKnownExclusions(), true); + } + + public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies) + where TAttribute : Attribute + { + return FindClassesOfTypeWithAttribute(assemblies, true); + } + + public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) + where TAttribute : Attribute + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface + && typeof(T).IsAssignableFrom(t) + && t.GetCustomAttributes(false).Any() + && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + + return l; + } + + /// + /// Searches all filtered local assemblies specified for classes of the type passed in. + /// + /// + /// + public static IEnumerable FindClassesOfType() + { + return FindClassesOfType(GetAssembliesWithKnownExclusions(), true); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfType(IEnumerable assemblies, bool onlyConcreteClasses) + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + + return GetAssignablesFromType(assemblies, onlyConcreteClasses); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + public static IEnumerable FindClassesOfType(IEnumerable assemblies) + { + return FindClassesOfType(assemblies, true); + } + + /// + /// Finds the classes with attribute. + /// + /// + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) + where T : Attribute + { + return FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); + } + + /// + /// Finds the classes with attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute(Type type, IEnumerable assemblies, bool onlyConcreteClasses) + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + if (!TypeHelper.IsTypeAssignableFrom(type)) + throw new ArgumentException("The type specified: " + type + " is not an Attribute type"); + + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface && t.GetCustomAttributes(type, false).Any() && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + + return l; + } + + /// + /// Finds the classes with attribute. + /// + /// + /// The assemblies. + /// + public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies) + where T : Attribute + { + return FindClassesWithAttribute(assemblies, true); + } + + /// + /// Finds the classes with attribute in filtered local assemblies + /// + /// + /// + public static IEnumerable FindClassesWithAttribute() + where T : Attribute + { + return FindClassesWithAttribute(GetAssembliesWithKnownExclusions()); + } + + + #region Private methods + + /// + /// Gets a collection of assignables of type T from a collection of assemblies + /// + /// + /// + /// + /// + private static IEnumerable GetAssignablesFromType(IEnumerable assemblies, bool onlyConcreteClasses) + { + return GetTypes(typeof(T), assemblies, onlyConcreteClasses); + } + + private static IEnumerable GetTypes(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses) + { + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface && assignTypeFrom.IsAssignableFrom(t) && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + return l; + } + + private static IEnumerable GetTypesWithFormattedException(Assembly a) + { + //if the assembly is dynamic, do not try to scan it + if (a.IsDynamic) + return Enumerable.Empty(); + + try + { + return a.GetExportedTypes(); + } + catch (ReflectionTypeLoadException ex) + { + 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.ToString()); + } + throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); + } + } + + #endregion + + + + } } + + } \ No newline at end of file diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 68df221c68..2fa8300d46 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -22,6 +22,24 @@ namespace Umbraco.Web public static class HtmlHelperRenderExtensions { /// + /// Renders a partial view that is found in the specified area + /// + /// + /// + /// + /// + /// + /// + public static MvcHtmlString AreaPartial(this HtmlHelper helper, string partial, string area, object model = null, ViewDataDictionary viewData = null) + { + var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; + helper.ViewContext.RouteData.DataTokens["area"] = area; + var result = helper.Partial(partial, model, viewData); + helper.ViewContext.RouteData.DataTokens["area"] = originalArea; + return result; + } + + /// /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are /// using does not inherit from UmbracoTemplatePage ///