2012-08-01 22:06:15 +06:00
using System ;
using System.Collections.Generic ;
2012-11-11 09:09:06 +05:00
using System.Globalization ;
using System.IO ;
2012-08-01 22:06:15 +06:00
using System.Linq ;
using System.Reflection ;
using System.Text ;
using System.Threading ;
2017-10-06 11:47:25 +11:00
using System.Web ;
2012-11-12 08:10:12 +05:00
using System.Web.Compilation ;
2017-03-03 13:26:20 +01:00
using Umbraco.Core.Cache ;
2012-11-11 09:09:06 +05:00
using Umbraco.Core.IO ;
2012-08-01 22:06:15 +06:00
using Umbraco.Core.Logging ;
2013-03-13 01:09:29 +04:00
using Umbraco.Core.Persistence.Mappers ;
2013-03-09 10:43:34 -01:00
using Umbraco.Core.Persistence.SqlSyntax ;
2015-01-16 15:47:44 +11:00
using Umbraco.Core.Profiling ;
2012-11-05 06:14:44 +06:00
using Umbraco.Core.PropertyEditors ;
2012-08-01 22:06:15 +06:00
using umbraco.interfaces ;
2017-10-06 11:47:25 +11:00
using Umbraco.Core.Configuration ;
2012-11-27 13:27:33 -01:00
using File = System . IO . File ;
2012-08-01 22:06:15 +06:00
namespace Umbraco.Core
{
2012-11-27 13:27:33 -01:00
/// <summary>
2017-03-03 13:26:20 +01:00
/// Provides methods to find and instanciate types.
2012-11-27 13:27:33 -01:00
/// </summary>
/// <remarks>
2017-03-03 13:26:20 +01:00
/// <para>This class should be used to resolve all types, the <see cref="TypeFinder"/> class should never be used directly.</para>
/// <para>In most cases this class is not used directly but through extension methods that retrieve specific types.</para>
/// <para>This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying
/// on a hash of the DLLs in the ~/bin folder to check for cache expiration.</para>
2012-11-27 13:27:33 -01:00
/// </remarks>
2014-02-17 15:08:27 +11:00
public class PluginManager
2012-11-27 13:27:33 -01:00
{
2017-03-03 13:26:20 +01:00
private const string CacheKey = "umbraco-plugins.list" ;
private static PluginManager _current ;
private static bool _hasCurrent ;
private static object _currentLock = new object ( ) ;
private readonly IServiceProvider _serviceProvider ;
private readonly IRuntimeCacheProvider _runtimeCache ;
private readonly ProfilingLogger _logger ;
private readonly string _tempFolder ;
2017-03-05 10:20:11 +01:00
private readonly object _typesLock = new object ( ) ;
private readonly Dictionary < TypeListKey , TypeList > _types = new Dictionary < TypeListKey , TypeList > ( ) ;
2017-03-03 13:26:20 +01:00
2017-08-17 11:38:57 +10:00
private string _cachedAssembliesHash = null ;
private string _currentAssembliesHash = null ;
2017-03-03 13:26:20 +01:00
private IEnumerable < Assembly > _assemblies ;
2017-03-05 10:20:11 +01:00
private bool _reportedChange ;
2017-03-03 13:26:20 +01:00
2012-11-27 13:27:33 -01:00
/// <summary>
2017-03-03 13:26:20 +01:00
/// Initializes a new instance of the <see cref="PluginManager"/> class.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-03 13:26:20 +01:00
/// <param name="serviceProvider">A mechanism for retrieving service objects.</param>
/// <param name="runtimeCache">The application runtime cache.</param>
/// <param name="logger">A profiling logger.</param>
2017-03-05 10:20:11 +01:00
/// <param name="detectChanges">Whether to detect changes using hashes.</param>
2015-01-16 15:47:44 +11:00
internal PluginManager ( IServiceProvider serviceProvider , IRuntimeCacheProvider runtimeCache , ProfilingLogger logger , bool detectChanges = true )
2012-11-27 13:27:33 -01:00
{
2015-01-16 15:47:44 +11:00
if ( serviceProvider = = null ) throw new ArgumentNullException ( "serviceProvider" ) ;
if ( runtimeCache = = null ) throw new ArgumentNullException ( "runtimeCache" ) ;
if ( logger = = null ) throw new ArgumentNullException ( "logger" ) ;
_serviceProvider = serviceProvider ;
_runtimeCache = runtimeCache ;
_logger = logger ;
2012-11-27 13:27:33 -01:00
2017-03-03 13:26:20 +01:00
// the temp folder where the cache file lives
2012-11-27 13:27:33 -01:00
_tempFolder = IOHelper . MapPath ( "~/App_Data/TEMP/PluginCache" ) ;
2015-01-16 15:47:44 +11:00
if ( Directory . Exists ( _tempFolder ) = = false )
2012-11-27 13:27:33 -01:00
Directory . CreateDirectory ( _tempFolder ) ;
2014-05-12 17:07:29 +10:00
var pluginListFile = GetPluginListFilePath ( ) ;
if ( detectChanges )
2012-11-27 13:27:33 -01:00
{
2017-08-17 11:38:57 +10:00
//first check if the cached hash is string.Empty, if it is then we need
2012-11-27 13:27:33 -01:00
//do the check if they've changed
2017-08-17 11:38:57 +10:00
RequiresRescanning = ( CachedAssembliesHash ! = CurrentAssembliesHash ) | | CachedAssembliesHash = = string . Empty ;
2012-11-27 13:27:33 -01:00
//if they have changed, we need to write the new file
2014-05-12 17:07:29 +10:00
if ( RequiresRescanning )
2017-10-06 11:51:27 +11:00
{
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all plugin types including lazy ones.
// http://issues.umbraco.org/issue/U4-4789
if ( File . Exists ( pluginListFile ) )
File . Delete ( pluginListFile ) ;
2014-05-12 17:07:29 +10:00
2012-11-27 13:27:33 -01:00
WriteCachePluginsHash ( ) ;
}
}
else
2017-10-06 11:51:27 +11:00
{
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all plugin types including lazy ones.
2014-05-12 17:07:29 +10:00
// http://issues.umbraco.org/issue/U4-4789
2017-10-06 11:51:27 +11:00
if ( File . Exists ( pluginListFile ) )
File . Delete ( pluginListFile ) ;
2014-05-12 17:07:29 +10:00
2017-03-05 10:20:11 +01:00
// always set to true if we're not detecting (generally only for testing)
2014-05-12 17:07:29 +10:00
RequiresRescanning = true ;
2012-11-27 13:27:33 -01:00
}
}
/// <summary>
2017-03-03 13:26:20 +01:00
/// Gets or sets the set of assemblies to scan.
2012-11-27 13:27:33 -01:00
/// </summary>
/// <remarks>
2017-03-03 13:26:20 +01:00
/// <para>If not explicitely set, defaults to all assemblies except those that are know to not have any of the
/// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies
/// for example.</para>
/// <para>This is for unit tests.</para>
2012-11-27 13:27:33 -01:00
/// </remarks>
2017-03-03 13:26:20 +01:00
internal IEnumerable < Assembly > AssembliesToScan
{
get { return _assemblies ? ? ( _assemblies = TypeFinder . GetAssembliesWithKnownExclusions ( ) ) ; }
set { _assemblies = value ; }
}
2017-03-05 10:20:11 +01:00
/// <summary>
/// Gets the type lists.
/// </summary>
/// <remarks>For unit tests.</remarks>
internal IEnumerable < TypeList > TypeLists
{
get { return _types . Values ; }
}
/// <summary>
/// Sets a type list.
/// </summary>
/// <remarks>For unit tests.</remarks>
internal void AddTypeList ( TypeList typeList )
{
_types [ new TypeListKey ( typeList . BaseType , typeList . AttributeType ) ] = typeList ;
}
2017-03-03 13:26:20 +01:00
/// <summary>
/// Gets or sets the singleton instance.
/// </summary>
/// <remarks>The setter exists for unit tests.</remarks>
2014-02-17 15:08:27 +11:00
public static PluginManager Current
2012-11-27 13:27:33 -01:00
{
get
{
2017-03-03 13:26:20 +01:00
return LazyInitializer . EnsureInitialized ( ref _current , ref _hasCurrent , ref _currentLock , ( ) = >
2012-11-27 13:27:33 -01:00
{
2017-03-03 13:26:20 +01:00
IRuntimeCacheProvider runtimeCache ;
ProfilingLogger profilingLogger ;
2015-01-16 15:47:44 +11:00
if ( ApplicationContext . Current = = null )
2012-11-27 13:27:33 -01:00
{
2017-03-03 13:26:20 +01:00
runtimeCache = new NullCacheProvider ( ) ;
2015-01-16 15:47:44 +11:00
var logger = LoggerResolver . HasCurrent ? LoggerResolver . Current . Logger : new DebugDiagnosticsLogger ( ) ;
var profiler = ProfilerResolver . HasCurrent ? ProfilerResolver . Current . Profiler : new LogProfiler ( logger ) ;
2017-03-03 13:26:20 +01:00
profilingLogger = new ProfilingLogger ( logger , profiler ) ;
2012-11-27 13:27:33 -01:00
}
2017-03-03 13:26:20 +01:00
else
{
runtimeCache = ApplicationContext . Current . ApplicationCache . RuntimeCache ;
profilingLogger = ApplicationContext . Current . ProfilingLogger ;
}
return new PluginManager ( new ActivatorServiceProvider ( ) , runtimeCache , profilingLogger ) ;
2015-01-16 15:47:44 +11:00
} ) ;
}
set
{
2017-03-03 13:26:20 +01:00
_hasCurrent = true ;
_current = value ;
2012-11-27 13:27:33 -01:00
}
}
2017-03-05 10:20:11 +01:00
#region Hashing
2012-11-27 13:27:33 -01:00
/// <summary>
2017-03-05 10:20:11 +01:00
/// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed.
2012-11-27 13:27:33 -01:00
/// </summary>
2014-05-12 17:07:29 +10:00
internal bool RequiresRescanning { get ; private set ; }
2012-11-27 13:27:33 -01:00
/// <summary>
2017-03-05 10:20:11 +01:00
/// Gets the currently cached hash value of the scanned assemblies.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-08-17 11:38:57 +10:00
/// <value>The cached hash value, or string.Empty if no cache is found.</value>
internal string CachedAssembliesHash
2012-11-27 13:27:33 -01:00
{
get
{
2017-08-17 11:38:57 +10:00
if ( _cachedAssembliesHash ! = null )
2012-11-27 13:27:33 -01:00
return _cachedAssembliesHash ;
2013-02-22 21:29:48 +06:00
var filePath = GetPluginHashFilePath ( ) ;
2017-08-17 11:38:57 +10:00
if ( File . Exists ( filePath ) = = false ) return string . Empty ;
2017-03-05 10:20:11 +01:00
2012-11-27 13:27:33 -01:00
var hash = File . ReadAllText ( filePath , Encoding . UTF8 ) ;
2017-09-13 18:23:19 +02:00
2017-08-17 11:38:57 +10:00
_cachedAssembliesHash = hash ;
2017-03-05 10:20:11 +01:00
return _cachedAssembliesHash ;
2012-11-27 13:27:33 -01:00
}
}
/// <summary>
2017-03-05 10:20:11 +01:00
/// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-05 10:20:11 +01:00
/// <value>The current hash.</value>
2017-08-17 11:38:57 +10:00
internal string CurrentAssembliesHash
2012-11-27 13:27:33 -01:00
{
get
{
2017-08-17 11:38:57 +10:00
if ( _currentAssembliesHash ! = null )
2012-11-27 13:27:33 -01:00
return _currentAssembliesHash ;
2017-03-05 10:20:11 +01:00
_currentAssembliesHash = GetFileHash ( new List < Tuple < FileSystemInfo , bool > >
{
// the bin folder and everything in it
new Tuple < FileSystemInfo , bool > ( new DirectoryInfo ( IOHelper . MapPath ( SystemDirectories . Bin ) ) , false ) ,
// the app code folder and everything in it
new Tuple < FileSystemInfo , bool > ( new DirectoryInfo ( IOHelper . MapPath ( "~/App_Code" ) ) , false ) ,
// global.asax (the app domain also monitors this, if it changes will do a full restart)
new Tuple < FileSystemInfo , bool > ( new FileInfo ( IOHelper . MapPath ( "~/global.asax" ) ) , false ) ,
// trees.config - use the contents to create the hash since this gets resaved on every app startup!
new Tuple < FileSystemInfo , bool > ( new FileInfo ( IOHelper . MapPath ( SystemDirectories . Config + "/trees.config" ) ) , true )
} , _logger ) ;
2012-11-27 13:27:33 -01:00
return _currentAssembliesHash ;
}
}
/// <summary>
2017-03-05 10:20:11 +01:00
/// Writes the assembly hash file.
2012-11-27 13:27:33 -01:00
/// </summary>
private void WriteCachePluginsHash ( )
{
2013-02-22 21:29:48 +06:00
var filePath = GetPluginHashFilePath ( ) ;
2012-11-27 13:27:33 -01:00
File . WriteAllText ( filePath , CurrentAssembliesHash . ToString ( ) , Encoding . UTF8 ) ;
}
/// <summary>
2017-03-05 10:20:11 +01:00
/// Returns a unique hash for a combination of FileInfo objects.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-05 10:20:11 +01:00
/// <param name="filesAndFolders">A collection of files.</param>
/// <param name="logger">A profiling logger.</param>
/// <returns>The hash.</returns>
/// <remarks>Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the
/// file properties (false) or the file contents (true).</remarks>
2017-08-17 11:38:57 +10:00
internal static string GetFileHash ( IEnumerable < Tuple < FileSystemInfo , bool > > filesAndFolders , ProfilingLogger logger )
2012-11-27 13:27:33 -01:00
{
2015-01-16 12:29:27 +11:00
using ( logger . TraceDuration < PluginManager > ( "Determining hash of code files on disk" , "Hash determined" ) )
2017-09-13 18:23:19 +02:00
{
2017-03-05 10:20:11 +01:00
// get the distinct file infos to hash
var uniqInfos = new HashSet < string > ( ) ;
var uniqContent = new HashSet < string > ( ) ;
2017-08-17 11:38:57 +10:00
using ( var generator = new HashGenerator ( ) )
2012-11-27 13:27:33 -01:00
{
2017-08-17 11:38:57 +10:00
foreach ( var fileOrFolder in filesAndFolders )
2017-03-05 10:20:11 +01:00
{
2017-08-17 11:38:57 +10:00
var info = fileOrFolder . Item1 ;
if ( fileOrFolder . Item2 )
{
// add each unique file's contents to the hash
// normalize the content for cr/lf and case-sensitivity
if ( uniqContent . Add ( info . FullName ) )
{
if ( File . Exists ( info . FullName ) = = false ) continue ;
var content = RemoveCrLf ( File . ReadAllText ( info . FullName ) ) ;
generator . AddCaseInsensitiveString ( content ) ;
}
}
else
{
// add each unique folder/file to the hash
if ( uniqInfos . Add ( info . FullName ) )
{
generator . AddFileSystemItem ( info ) ;
}
}
2017-03-05 10:20:11 +01:00
}
2017-08-17 11:38:57 +10:00
return generator . GenerateHash ( ) ;
2017-09-13 18:23:19 +02:00
}
2017-03-05 10:20:11 +01:00
}
}
// fast! (yes, according to benchmarks)
private static string RemoveCrLf ( string s )
{
var buffer = new char [ s . Length ] ;
var count = 0 ;
// ReSharper disable once ForCanBeConvertedToForeach - no!
for ( var i = 0 ; i < s . Length ; i + + )
{
if ( s [ i ] ! = '\r' & & s [ i ] ! = '\n' )
buffer [ count + + ] = s [ i ] ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
return new string ( buffer , 0 , count ) ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
/// <summary>
/// Returns a unique hash for a combination of FileInfo objects.
/// </summary>
/// <param name="filesAndFolders">A collection of files.</param>
/// <param name="logger">A profiling logger.</param>
/// <returns>The hash.</returns>
2017-08-17 11:38:57 +10:00
internal static string GetFileHash ( IEnumerable < FileSystemInfo > filesAndFolders , ProfilingLogger logger )
2013-11-21 14:42:35 +11:00
{
2015-01-16 12:29:27 +11:00
using ( logger . TraceDuration < PluginManager > ( "Determining hash of code files on disk" , "Hash determined" ) )
2013-11-21 14:42:35 +11:00
{
2017-08-17 11:38:57 +10:00
using ( var generator = new HashGenerator ( ) )
2013-11-21 14:42:35 +11:00
{
2017-08-17 11:38:57 +10:00
// get the distinct file infos to hash
var uniqInfos = new HashSet < string > ( ) ;
2017-03-05 10:20:11 +01:00
2017-08-17 11:38:57 +10:00
foreach ( var fileOrFolder in filesAndFolders )
{
if ( uniqInfos . Contains ( fileOrFolder . FullName ) ) continue ;
uniqInfos . Add ( fileOrFolder . FullName ) ;
generator . AddFileSystemItem ( fileOrFolder ) ;
}
return generator . GenerateHash ( ) ;
2017-09-13 18:23:19 +02:00
}
2013-11-21 14:42:35 +11:00
}
2017-09-13 18:23:19 +02:00
}
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
#endregion
#region Cache
2017-07-31 16:24:22 +02:00
private const int ListFileOpenReadTimeout = 4000 ; // milliseconds
private const int ListFileOpenWriteTimeout = 2000 ; // milliseconds
2012-11-27 13:27:33 -01:00
/// <summary>
2017-03-05 10:20:11 +01:00
/// Attemps to retrieve the list of types from the cache.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-05 10:20:11 +01:00
/// <remarks>Fails if the cache is missing or corrupt in any way.</remarks>
internal Attempt < IEnumerable < string > > TryGetCached ( Type baseType , Type attributeType )
{
2017-04-03 09:25:27 +02:00
var cache = _runtimeCache . GetCacheItem < Dictionary < Tuple < string , string > , IEnumerable < string > > > ( CacheKey , ReadCacheSafe , TimeSpan . FromMinutes ( 4 ) ) ;
2017-03-05 10:20:11 +01:00
IEnumerable < string > types ;
cache . TryGetValue ( Tuple . Create ( baseType = = null ? string . Empty : baseType . FullName , attributeType = = null ? string . Empty : attributeType . FullName ) , out types ) ;
return types = = null
? Attempt < IEnumerable < string > > . Fail ( )
: Attempt . Succeed ( types ) ;
}
2017-04-03 09:25:27 +02:00
internal Dictionary < Tuple < string , string > , IEnumerable < string > > ReadCacheSafe ( )
{
try
{
return ReadCache ( ) ;
}
catch
{
try
{
var filePath = GetPluginListFilePath ( ) ;
File . Delete ( filePath ) ;
}
catch
{
// on-purpose, does not matter
}
}
return new Dictionary < Tuple < string , string > , IEnumerable < string > > ( ) ;
}
2017-03-05 10:20:11 +01:00
internal Dictionary < Tuple < string , string > , IEnumerable < string > > ReadCache ( )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
var cache = new Dictionary < Tuple < string , string > , IEnumerable < string > > ( ) ;
2012-11-27 13:27:33 -01:00
var filePath = GetPluginListFilePath ( ) ;
2017-03-05 10:20:11 +01:00
if ( File . Exists ( filePath ) = = false )
return cache ;
2012-11-27 13:27:33 -01:00
2017-07-31 16:24:22 +02:00
using ( var stream = GetFileStream ( filePath , FileMode . Open , FileAccess . Read , FileShare . Read , ListFileOpenReadTimeout ) )
2017-03-05 10:20:11 +01:00
using ( var reader = new StreamReader ( stream ) )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
while ( true )
{
var baseType = reader . ReadLine ( ) ;
if ( baseType = = null ) return cache ; // exit
if ( baseType . StartsWith ( "<" ) ) break ; // old xml
var attributeType = reader . ReadLine ( ) ;
if ( attributeType = = null ) break ;
var types = new List < string > ( ) ;
while ( true )
{
var type = reader . ReadLine ( ) ;
if ( type = = null )
{
types = null ; // break 2 levels
break ;
}
if ( type = = string . Empty )
{
cache [ Tuple . Create ( baseType , attributeType ) ] = types ;
break ;
}
types . Add ( type ) ;
}
if ( types = = null ) break ;
}
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
cache . Clear ( ) ;
return cache ;
2012-11-27 13:27:33 -01:00
}
2013-02-22 21:29:48 +06:00
/// <summary>
2017-03-05 10:20:11 +01:00
/// Removes cache files and internal cache.
2013-02-22 21:29:48 +06:00
/// </summary>
2017-03-05 10:20:11 +01:00
/// <remarks>Generally only used for resetting cache, for example during the install process.</remarks>
2014-05-12 17:07:29 +10:00
public void ClearPluginCache ( )
2013-02-22 21:29:48 +06:00
{
var path = GetPluginListFilePath ( ) ;
if ( File . Exists ( path ) )
File . Delete ( path ) ;
2017-03-05 10:20:11 +01:00
2013-02-22 21:29:48 +06:00
path = GetPluginHashFilePath ( ) ;
if ( File . Exists ( path ) )
File . Delete ( path ) ;
2015-01-16 15:47:44 +11:00
_runtimeCache . ClearCacheItem ( CacheKey ) ;
2013-02-22 21:29:48 +06:00
}
2017-03-03 13:26:20 +01:00
2012-11-27 13:27:33 -01:00
private string GetPluginListFilePath ( )
2017-10-06 11:47:25 +11:00
{
switch ( GlobalSettings . LocalTempStorageLocation )
{
case LocalTempStorage . AspNetTemp :
return Path . Combine ( HttpRuntime . CodegenDir , "umbraco-plugins.list" ) ;
case LocalTempStorage . EnvironmentTemp :
var appDomainHash = HttpRuntime . AppDomainAppId . ToSHA1 ( ) ;
var cachePath = Path . Combine ( Environment . ExpandEnvironmentVariables ( "%temp%" ) , "UmbracoPlugins" ,
//include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
// to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
// utilizing an old path
appDomainHash ) ;
return Path . Combine ( cachePath , "umbraco-plugins.list" ) ;
case LocalTempStorage . Default :
default :
return Path . Combine ( _tempFolder , "umbraco-plugins." + NetworkHelper . FileSafeMachineName + ".list" ) ;
}
2012-11-27 13:27:33 -01:00
}
2013-02-22 21:29:48 +06:00
private string GetPluginHashFilePath ( )
{
2017-03-05 10:20:11 +01:00
var filename = "umbraco-plugins." + NetworkHelper . FileSafeMachineName + ".hash" ;
return Path . Combine ( _tempFolder , filename ) ;
2013-02-22 21:29:48 +06:00
}
2017-03-05 10:20:11 +01:00
internal void WriteCache ( )
2012-11-27 13:27:33 -01:00
{
2017-09-13 18:23:19 +02:00
// be absolutely sure
if ( Directory . Exists ( _tempFolder ) = = false )
Directory . CreateDirectory ( _tempFolder ) ;
2012-11-27 13:27:33 -01:00
var filePath = GetPluginListFilePath ( ) ;
2017-07-31 16:24:22 +02:00
using ( var stream = GetFileStream ( filePath , FileMode . Create , FileAccess . Write , FileShare . None , ListFileOpenWriteTimeout ) )
2017-03-05 10:20:11 +01:00
using ( var writer = new StreamWriter ( stream ) )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
foreach ( var typeList in _types . Values )
{
writer . WriteLine ( typeList . BaseType = = null ? string . Empty : typeList . BaseType . FullName ) ;
writer . WriteLine ( typeList . AttributeType = = null ? string . Empty : typeList . AttributeType . FullName ) ;
foreach ( var type in typeList . Types )
writer . WriteLine ( type . AssemblyQualifiedName ) ;
writer . WriteLine ( ) ;
}
2012-11-27 13:27:33 -01:00
}
}
2017-03-05 10:20:11 +01:00
internal void UpdateCache ( )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
// note
// at the moment we write the cache to disk every time we update it. ideally we defer the writing
// since all the updates are going to happen in a row when Umbraco starts. that being said, the
// file is small enough, so it is not a priority.
2017-07-31 16:24:22 +02:00
WriteCache ( ) ;
}
private static Stream GetFileStream ( string path , FileMode fileMode , FileAccess fileAccess , FileShare fileShare , int timeoutMilliseconds )
{
const int pauseMilliseconds = 250 ;
var attempts = timeoutMilliseconds / pauseMilliseconds ;
while ( true )
{
try
2017-08-01 09:16:39 +02:00
{
2017-07-31 16:24:22 +02:00
return new FileStream ( path , fileMode , fileAccess , fileShare ) ;
}
catch
{
if ( - - attempts = = 0 )
2017-08-01 09:16:39 +02:00
throw ;
LogHelper . Debug < PluginManager > ( string . Format ( "Attempted to get filestream for file {0} failed, {1} attempts left, pausing for {2} milliseconds" , path , attempts , pauseMilliseconds ) ) ;
2017-07-31 16:38:39 +02:00
Thread . Sleep ( pauseMilliseconds ) ;
2017-07-31 16:24:22 +02:00
}
}
2012-11-27 13:27:33 -01:00
}
#endregion
2017-03-03 13:26:20 +01:00
#region Create Instances
2013-05-23 22:15:52 -10:00
2013-01-29 09:45:12 +06:00
/// <summary>
2017-03-03 13:26:20 +01:00
/// Resolves and creates instances.
2013-01-29 09:45:12 +06:00
/// </summary>
2017-03-03 13:26:20 +01:00
/// <typeparam name="T">The type to use for resolution.</typeparam>
/// <param name="throwException">Indicates whether to throw if an instance cannot be created.</param>
2017-03-05 10:20:11 +01:00
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
2017-03-03 13:26:20 +01:00
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>The created instances.</returns>
/// <remarks>
/// <para>By default <paramref name="throwException"/> is false and instances that cannot be created are just skipped.</para>
2017-03-05 10:20:11 +01:00
/// <para>By default <paramref name="cache"/> is true and cache is used for type resolution.</para>
2017-03-03 13:26:20 +01:00
/// <para>By default <paramref name="specificAssemblies"/> is null and <see cref="AssembliesToScan"/> is used.</para>
2017-03-05 10:20:11 +01:00
/// <para>Caching is disabled when using specific assemblies.</para>
2017-03-03 13:26:20 +01:00
/// </remarks>
2017-03-05 10:20:11 +01:00
internal IEnumerable < T > FindAndCreateInstances < T > ( bool throwException = false , bool cache = true , IEnumerable < Assembly > specificAssemblies = null )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
var types = ResolveTypes < T > ( cache , specificAssemblies ) ;
2012-11-27 13:27:33 -01:00
return CreateInstances < T > ( types , throwException ) ;
}
/// <summary>
2017-03-03 13:26:20 +01:00
/// Creates instances of the specified types.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-03 13:26:20 +01:00
/// <typeparam name="T">The base type for all instances.</typeparam>
/// <param name="types">The instance types.</param>
/// <param name="throwException">Indicates whether to throw if an instance cannot be created.</param>
/// <returns>The created instances.</returns>
/// <remarks>By default <paramref name="throwException"/> is false and instances that cannot be created are just skipped.</remarks>
2012-11-27 13:27:33 -01:00
internal IEnumerable < T > CreateInstances < T > ( IEnumerable < Type > types , bool throwException = false )
{
2015-01-16 15:47:44 +11:00
return _serviceProvider . CreateInstances < T > ( types , _logger . Logger , throwException ) ;
2012-11-27 13:27:33 -01:00
}
/// <summary>
2017-03-03 13:26:20 +01:00
/// Creates an instance of the specified type.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-03 13:26:20 +01:00
/// <typeparam name="T">The base type of the instance.</typeparam>
/// <param name="type">The type of the instance.</param>
2012-11-27 13:27:33 -01:00
/// <param name="throwException"></param>
2017-03-03 13:26:20 +01:00
/// <returns>The created instance.</returns>
2012-11-27 13:27:33 -01:00
internal T CreateInstance < T > ( Type type , bool throwException = false )
{
var instances = CreateInstances < T > ( new [ ] { type } , throwException ) ;
return instances . FirstOrDefault ( ) ;
}
2017-03-03 13:26:20 +01:00
#endregion
#region Resolve Types
2017-03-05 10:20:11 +01:00
/// <summary>
/// Resolves class types inheriting from or implementing the specified type
/// </summary>
/// <typeparam name="T">The type to inherit from or implement.</typeparam>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types inheriting from or implementing the specified type.</returns>
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable < Type > ResolveTypes < T > ( bool cache = true , IEnumerable < Assembly > specificAssemblies = null )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
// do not cache anything from specific assemblies
cache & = specificAssemblies = = null ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
// if not caching, or not IDiscoverable, directly resolve types
if ( cache = = false | | typeof ( IDiscoverable ) . IsAssignableFrom ( typeof ( T ) ) = = false )
{
return ResolveTypesInternal (
typeof ( T ) , null ,
( ) = > TypeFinder . FindClassesOfType < T > ( specificAssemblies ? ? AssembliesToScan ) ,
cache ) ;
}
2013-12-06 14:06:53 +11:00
2017-03-05 10:20:11 +01:00
// if caching and IDiscoverable
// filter the cached discovered types (and cache the result)
2013-12-06 14:06:53 +11:00
2017-03-05 10:20:11 +01:00
var discovered = ResolveTypesInternal (
typeof ( IDiscoverable ) , null ,
( ) = > TypeFinder . FindClassesOfType < IDiscoverable > ( AssembliesToScan ) ,
true ) ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
return ResolveTypesInternal (
typeof ( T ) , null ,
( ) = > discovered
. Where ( x = > typeof ( T ) . IsAssignableFrom ( x ) ) ,
true ) ;
2012-11-27 13:27:33 -01:00
}
/// <summary>
2017-03-05 10:20:11 +01:00
/// Resolves class types inheriting from or implementing the specified type and marked with the specified attribute.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-05 10:20:11 +01:00
/// <typeparam name="T">The type to inherit from or implement.</typeparam>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types inheriting from or implementing the specified type and marked with the specified attribute.</returns>
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable < Type > ResolveTypesWithAttribute < T , TAttribute > ( bool cache = true , IEnumerable < Assembly > specificAssemblies = null )
where TAttribute : Attribute
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
// do not cache anything from specific assemblies
cache & = specificAssemblies = = null ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
// if not caching, or not IDiscoverable, directly resolve types
if ( cache = = false | | typeof ( IDiscoverable ) . IsAssignableFrom ( typeof ( T ) ) = = false )
2017-02-28 23:12:48 +01:00
{
2017-03-05 10:20:11 +01:00
return ResolveTypesInternal (
typeof ( T ) , typeof ( TAttribute ) ,
( ) = > TypeFinder . FindClassesOfTypeWithAttribute < T , TAttribute > ( specificAssemblies ? ? AssembliesToScan ) ,
cache ) ;
2017-02-28 23:12:48 +01:00
}
2017-03-05 10:20:11 +01:00
// if caching and IDiscoverable
// filter the cached discovered types (and cache the result)
2017-02-28 23:12:48 +01:00
2017-03-05 10:20:11 +01:00
var discovered = ResolveTypesInternal (
typeof ( IDiscoverable ) , null ,
( ) = > TypeFinder . FindClassesOfType < IDiscoverable > ( AssembliesToScan ) ,
true ) ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
return ResolveTypesInternal (
typeof ( T ) , typeof ( TAttribute ) ,
( ) = > discovered
. Where ( x = > typeof ( T ) . IsAssignableFrom ( x ) )
. Where ( x = > x . GetCustomAttributes < TAttribute > ( false ) . Any ( ) ) ,
true ) ;
2012-11-27 13:27:33 -01:00
}
/// <summary>
2017-03-03 13:26:20 +01:00
/// Resolves class types marked with the specified attribute.
2012-11-27 13:27:33 -01:00
/// </summary>
2017-03-03 13:26:20 +01:00
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
2017-03-05 10:20:11 +01:00
/// <param name="cache">Indicates whether to use cache for type resolution.</param>
2017-03-03 13:26:20 +01:00
/// <param name="specificAssemblies">A set of assemblies for type resolution.</param>
/// <returns>All class types marked with the specified attribute.</returns>
2017-03-05 10:20:11 +01:00
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable < Type > ResolveAttributedTypes < TAttribute > ( bool cache = true , IEnumerable < Assembly > specificAssemblies = null )
2012-11-27 13:27:33 -01:00
where TAttribute : Attribute
{
2017-03-05 10:20:11 +01:00
// do not cache anything from specific assemblies
cache & = specificAssemblies = = null ;
return ResolveTypesInternal (
typeof ( object ) , typeof ( TAttribute ) ,
2014-02-17 15:58:57 +11:00
( ) = > TypeFinder . FindClassesWithAttribute < TAttribute > ( specificAssemblies ? ? AssembliesToScan ) ,
2017-03-05 10:20:11 +01:00
cache ) ;
2017-03-03 13:26:20 +01:00
}
2017-03-05 10:20:11 +01:00
private IEnumerable < Type > ResolveTypesInternal (
Type baseType , Type attributeType ,
Func < IEnumerable < Type > > finder ,
bool cache )
{
// using an upgradeable lock makes little sense here as only one thread can enter the upgradeable
// lock at a time, and we don't have non-upgradeable readers, and quite probably the plugin
// manager is mostly not going to be used in any kind of massively multi-threaded scenario - so,
// a plain lock is enough
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
var name = ResolvedName ( baseType , attributeType ) ;
lock ( _typesLock )
using ( _logger . TraceDuration < PluginManager > (
"Resolving " + name ,
"Resolved " + name ) ) // cannot contain typesFound.Count as it's evaluated before the find
{
// resolve within a lock & timer
return ResolveTypesInternalLocked ( baseType , attributeType , finder , cache ) ;
}
}
private static string ResolvedName ( Type baseType , Type attributeType )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
var s = attributeType = = null ? string . Empty : ( "[" + attributeType + "]" ) ;
s + = baseType ;
return s ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
private IEnumerable < Type > ResolveTypesInternalLocked (
Type baseType , Type attributeType ,
Func < IEnumerable < Type > > finder ,
bool cache )
{
// check if the TypeList already exists, if so return it, if not we'll create it
var listKey = new TypeListKey ( baseType , attributeType ) ;
TypeList typeList = null ;
if ( cache )
_types . TryGetValue ( listKey , out typeList ) ; // else null
// if caching and found, return
if ( typeList ! = null )
{
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
_logger . Logger . Debug < PluginManager > ( "Resolving {0}: found a cached type list." , ( ) = > ResolvedName ( baseType , attributeType ) ) ;
return typeList . Types ;
}
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
// else proceed,
typeList = new TypeList ( baseType , attributeType ) ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
var scan = RequiresRescanning | | File . Exists ( GetPluginListFilePath ( ) ) = = false ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
if ( scan )
{
// either we have to rescan, or we could not find the cache file:
2017-04-03 21:15:50 +10:00
// report (only once) and scan and update the cache file - this variable is purely used to check if we need to log
2017-03-05 10:20:11 +01:00
if ( _reportedChange = = false )
{
2017-04-03 21:15:50 +10:00
_logger . Logger . Debug < PluginManager > ( "Assembly changes detected, need to rescan everything." ) ;
2017-03-05 10:20:11 +01:00
_reportedChange = true ;
}
}
if ( scan = = false )
{
// if we don't have to scan, try the cache
var cacheResult = TryGetCached ( baseType , attributeType ) ;
// here we need to identify if the CachedPluginNotFoundInFile was the exception, if it was then we need to re-scan
// in some cases the plugin will not have been scanned for on application startup, but the assemblies haven't changed
// so in this instance there will never be a result.
if ( cacheResult . Exception is CachedPluginNotFoundInFileException | | cacheResult . Success = = false )
{
_logger . Logger . Debug < PluginManager > ( "Resolving {0}: failed to load from cache file, must scan assemblies." , ( ) = > ResolvedName ( baseType , attributeType ) ) ;
scan = true ;
}
else
{
// successfully retrieved types from the file cache: load
foreach ( var type in cacheResult . Result )
{
try
{
// we use the build manager to ensure we get all types loaded, this is slightly slower than
// Type.GetType but if the types in the assembly aren't loaded yet it would fail whereas
// BuildManager will load them - this is how eg MVC loads types, etc - no need to make it
// more complicated
typeList . Add ( BuildManager . GetType ( type , true ) ) ;
}
catch ( Exception ex )
{
// in case of any exception, we have to exit, and revert to scanning
_logger . Logger . Error < PluginManager > ( "Resolving " + ResolvedName ( baseType , attributeType ) + ": failed to load cache file type " + type + ", reverting to scanning assemblies." , ex ) ;
scan = true ;
break ;
}
}
if ( scan = = false )
{
_logger . Logger . Debug < PluginManager > ( "Resolving {0}: loaded types from cache file." , ( ) = > ResolvedName ( baseType , attributeType ) ) ;
}
}
}
if ( scan )
{
// either we had to scan, or we could not resolve the types from the cache file - scan now
_logger . Logger . Debug < PluginManager > ( "Resolving {0}: scanning assemblies." , ( ) = > ResolvedName ( baseType , attributeType ) ) ;
foreach ( var t in finder ( ) )
typeList . Add ( t ) ;
}
// if we are to cache the results, do so
if ( cache )
{
var added = _types . ContainsKey ( listKey ) = = false ;
if ( added )
{
_types [ listKey ] = typeList ;
2017-04-03 21:15:50 +10:00
//if we are scanning then update the cache file
if ( scan )
{
UpdateCache ( ) ;
}
2017-03-05 10:20:11 +01:00
}
_logger . Logger . Debug < PluginManager > ( "Resolved {0}, caching ({1})." , ( ) = > ResolvedName ( baseType , attributeType ) , ( ) = > added . ToString ( ) . ToLowerInvariant ( ) ) ;
}
else
{
_logger . Logger . Debug < PluginManager > ( "Resolved {0}." , ( ) = > ResolvedName ( baseType , attributeType ) ) ;
}
return typeList . Types ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
#endregion
#region Nested classes and stuff
2017-03-03 13:26:20 +01:00
/// <summary>
2017-03-05 10:20:11 +01:00
/// Groups a type and a resolution kind into a key.
2017-03-03 13:26:20 +01:00
/// </summary>
2017-03-05 10:20:11 +01:00
private struct TypeListKey
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
// ReSharper disable MemberCanBePrivate.Local
public readonly Type BaseType ;
public readonly Type AttributeType ;
// ReSharper restore MemberCanBePrivate.Local
2017-03-03 13:26:20 +01:00
2017-03-05 10:20:11 +01:00
public TypeListKey ( Type baseType , Type attributeType )
{
BaseType = baseType ? ? typeof ( object ) ;
AttributeType = attributeType ;
}
2017-03-03 13:26:20 +01:00
2017-03-05 10:20:11 +01:00
public override bool Equals ( object obj )
{
if ( obj = = null | | obj is TypeListKey = = false ) return false ;
var o = ( TypeListKey ) obj ;
return BaseType = = o . BaseType & & AttributeType = = o . AttributeType ;
}
public override int GetHashCode ( )
{
// in case AttributeType is null we need something else, using typeof (TypeListKey)
// which does not really "mean" anything, it's just a value...
var hash = 5381 ;
hash = ( ( hash < < 5 ) + hash ) ^ BaseType . GetHashCode ( ) ;
hash = ( ( hash < < 5 ) + hash ) ^ ( AttributeType ? ? typeof ( TypeListKey ) ) . GetHashCode ( ) ;
return hash ;
}
2012-11-27 13:27:33 -01:00
}
2017-03-03 13:26:20 +01:00
/// <summary>
/// Represents a list of types obtained by looking for types inheriting/implementing a
/// specified type, and/or marked with a specified attribute type.
/// </summary>
2017-03-05 10:20:11 +01:00
internal class TypeList
2012-11-27 13:27:33 -01:00
{
2017-03-03 13:26:20 +01:00
private readonly HashSet < Type > _types = new HashSet < Type > ( ) ;
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
public TypeList ( Type baseType , Type attributeType )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
BaseType = baseType ;
AttributeType = attributeType ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
public Type BaseType { get ; private set ; }
public Type AttributeType { get ; private set ; }
2012-11-27 13:27:33 -01:00
2017-03-05 10:20:11 +01:00
/// <summary>
/// Adds a type.
/// </summary>
public void Add ( Type type )
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
if ( BaseType . IsAssignableFrom ( type ) = = false )
throw new ArgumentException ( "Base type " + BaseType + " is not assignable from type " + type + "." , "type" ) ;
_types . Add ( type ) ;
2012-11-27 13:27:33 -01:00
}
2017-03-05 10:20:11 +01:00
/// <summary>
/// Gets the types.
/// </summary>
public IEnumerable < Type > Types
2012-11-27 13:27:33 -01:00
{
2017-03-05 10:20:11 +01:00
get { return _types ; }
2012-11-27 13:27:33 -01:00
}
}
/// <summary>
2017-03-03 13:26:20 +01:00
/// Represents the error that occurs when a plugin was not found in the cache plugin
/// list with the specified TypeResolutionKind.
2012-11-27 13:27:33 -01:00
/// </summary>
2013-12-06 14:06:53 +11:00
internal class CachedPluginNotFoundInFileException : Exception
2017-03-03 13:26:20 +01:00
{ }
#endregion
}
internal static class PluginManagerExtensions
{
/// <summary>
/// Gets all classes inheriting from PropertyEditor.
/// </summary>
/// <remarks>
/// <para>Excludes the actual PropertyEditor base type.</para>
/// </remarks>
public static IEnumerable < Type > ResolvePropertyEditors ( this PluginManager mgr )
2012-11-27 13:27:33 -01:00
{
2017-03-03 13:26:20 +01:00
// look for IParameterEditor (fast, IDiscoverable) then filter
var propertyEditor = typeof ( PropertyEditor ) ;
2012-11-27 13:27:33 -01:00
2017-03-03 13:26:20 +01:00
return mgr . ResolveTypes < IParameterEditor > ( )
. Where ( x = > propertyEditor . IsAssignableFrom ( x ) & & x ! = propertyEditor ) ;
2012-11-27 13:27:33 -01:00
}
2017-03-03 13:26:20 +01:00
/// <summary>
/// Gets all classes implementing IParameterEditor.
/// </summary>
/// <remarks>
/// <para>Includes property editors.</para>
/// <para>Excludes the actual ParameterEditor and PropertyEditor base types.</para>
/// </remarks>
public static IEnumerable < Type > ResolveParameterEditors ( this PluginManager mgr )
{
var propertyEditor = typeof ( PropertyEditor ) ;
var parameterEditor = typeof ( ParameterEditor ) ;
return mgr . ResolveTypes < IParameterEditor > ( )
. Where ( x = > x ! = propertyEditor & & x ! = parameterEditor ) ;
}
/// <summary>
/// Gets all classes implementing IApplicationStartupHandler.
/// </summary>
[Obsolete("IApplicationStartupHandler is obsolete.")]
public static IEnumerable < Type > ResolveApplicationStartupHandlers ( this PluginManager mgr )
{
return mgr . ResolveTypes < IApplicationStartupHandler > ( ) ;
}
/// <summary>
/// Gets all classes implementing ICacheRefresher.
/// </summary>
public static IEnumerable < Type > ResolveCacheRefreshers ( this PluginManager mgr )
{
return mgr . ResolveTypes < ICacheRefresher > ( ) ;
}
/// <summary>
/// Gets all classes implementing IPropertyEditorValueConverter.
/// </summary>
[Obsolete("IPropertyEditorValueConverter is obsolete.")]
public static IEnumerable < Type > ResolvePropertyEditorValueConverters ( this PluginManager mgr )
{
return mgr . ResolveTypes < IPropertyEditorValueConverter > ( ) ;
}
/// <summary>
/// Gets all classes implementing IDataType.
/// </summary>
[Obsolete("IDataType is obsolete.")]
public static IEnumerable < Type > ResolveDataTypes ( this PluginManager mgr )
{
return mgr . ResolveTypes < IDataType > ( ) ;
}
/// <summary>
/// Gets all classes implementing IMacroGuiRendering.
/// </summary>
[Obsolete("IMacroGuiRendering is obsolete.")]
public static IEnumerable < Type > ResolveMacroRenderings ( this PluginManager mgr )
{
return mgr . ResolveTypes < IMacroGuiRendering > ( ) ;
}
/// <summary>
/// Gets all classes implementing IPackageAction.
/// </summary>
public static IEnumerable < Type > ResolvePackageActions ( this PluginManager mgr )
{
return mgr . ResolveTypes < IPackageAction > ( ) ;
}
/// <summary>
/// Gets all classes implementing IAction.
/// </summary>
public static IEnumerable < Type > ResolveActions ( this PluginManager mgr )
{
return mgr . ResolveTypes < IAction > ( ) ;
}
/// <summary>
/// Gets all classes inheriting from BaseMapper and marked with the MapperForAttribute.
/// </summary>
public static IEnumerable < Type > ResolveAssignedMapperTypes ( this PluginManager mgr )
{
return mgr . ResolveTypesWithAttribute < BaseMapper , MapperForAttribute > ( ) ;
}
/// <summary>
/// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute.
/// </summary>
public static IEnumerable < Type > ResolveSqlSyntaxProviders ( this PluginManager mgr )
{
return mgr . ResolveTypesWithAttribute < ISqlSyntaxProvider , SqlSyntaxProviderAttribute > ( ) ;
}
2012-11-27 13:27:33 -01:00
}
2012-08-01 22:06:15 +06:00
}