1
// -----------------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 // -----------------------------------------------------------------------
7 using System
.Collections
.Generic
;
8 using System
.Collections
.ObjectModel
;
9 using System
.ComponentModel
.Composition
.Diagnostics
;
10 using System
.ComponentModel
.Composition
.Primitives
;
11 using System
.Diagnostics
;
12 using System
.Diagnostics
.CodeAnalysis
;
13 using System
.Globalization
;
16 using System
.Reflection
;
17 using Microsoft
.Internal
;
18 using Microsoft
.Internal
.Collections
;
20 using IOPath
= System
.IO
.Path
;
22 namespace System
.ComponentModel
.Composition
.Hosting
24 [DebuggerTypeProxy(typeof(DirectoryCatalogDebuggerProxy
))]
25 public partial class DirectoryCatalog
: ComposablePartCatalog
, INotifyComposablePartCatalogChanged
, ICompositionElement
27 private readonly Lock _thisLock
= new Lock();
28 private ComposablePartCatalogCollection _catalogCollection
;
29 private Dictionary
<string, AssemblyCatalog
> _assemblyCatalogs
;
30 private volatile bool _isDisposed
= false;
32 private string _fullPath
;
33 private string _searchPattern
;
34 private ReadOnlyCollection
<string> _loadedFiles
;
35 private IQueryable
<ComposablePartDefinition
> _partsQuery
;
38 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the *.dll files
39 /// in the given directory path.
41 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
42 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
44 /// <param name="path">
45 /// Path to the directory to scan for assemblies to add to the catalog.
46 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
48 /// <exception cref="ArgumentException">
49 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
50 /// contains one or more implementation-specific invalid characters.
52 /// <exception cref="ArgumentNullException">
53 /// <paramref name="path"/> is <see langword="null"/>.
55 /// <exception cref="DirectoryNotFoundException">
56 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
58 /// <exception cref="PathTooLongException">
59 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
60 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
61 /// be less than 260 characters.
63 /// <exception cref="UnauthorizedAccessException">
64 /// The caller does not have the required permission.
66 public DirectoryCatalog(string path
) : this(path
, "*.dll")
71 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the given searchPattern
72 /// over the files in the given directory path.
74 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
75 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
77 /// <param name="path">
78 /// Path to the directory to scan for assemblies to add to the catalog.
79 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
81 /// <param name="searchPattern">
82 /// Any valid searchPattern that <see cref="Directory.GetFiles(string, string)"/> will accept.
84 /// <exception cref="ArgumentException">
85 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
86 /// contains one or more implementation-specific invalid characters. Or <paramref name="searchPattern"/>
87 /// does not contain a valid pattern.
89 /// <exception cref="ArgumentNullException">
90 /// <paramref name="path"/> is <see langword="null"/> or <paramref name="searchPattern"/> is <see langword="null"/>.
92 /// <exception cref="DirectoryNotFoundException">
93 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
95 /// <exception cref="PathTooLongException">
96 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
97 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
98 /// be less than 260 characters.
100 /// <exception cref="UnauthorizedAccessException">
101 /// The caller does not have the required permission.
103 public DirectoryCatalog(string path
, string searchPattern
)
105 Requires
.NotNullOrEmpty(path
, "path");
106 this.Initialize(path
, searchPattern
);
110 /// Translated absolute path of the path passed into the constructor of <see cref="DirectoryCatalog"/>.
112 public string FullPath
116 return this._fullPath
;
121 /// Set of files that have currently been loaded into the catalog.
123 public ReadOnlyCollection
<string> LoadedFiles
127 using (new ReadLock(this._thisLock
))
129 return this._loadedFiles
;
135 /// Path passed into the constructor of <see cref="DirectoryCatalog"/>.
146 /// Gets the part definitions of the directory catalog.
149 /// A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the
150 /// <see cref="DirectoryCatalog"/>.
152 /// <exception cref="ObjectDisposedException">
153 /// The <see cref="DirectoryCatalog"/> has been disposed of.
155 public override IQueryable
<ComposablePartDefinition
> Parts
159 this.ThrowIfDisposed();
160 return this._partsQuery
;
165 /// SearchPattern passed into the constructor of <see cref="DirectoryCatalog"/>, or the default *.dll.
167 public string SearchPattern
171 return this._searchPattern
;
176 /// Notify when the contents of the Catalog has changed.
178 public event EventHandler
<ComposablePartCatalogChangeEventArgs
> Changed
;
181 /// Notify when the contents of the Catalog has changing.
183 public event EventHandler
<ComposablePartCatalogChangeEventArgs
> Changing
;
186 /// Releases unmanaged and - optionally - managed resources
188 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
189 protected override void Dispose(bool disposing
)
195 if (!this._isDisposed
)
197 bool disposeLock
= false;
198 ComposablePartCatalogCollection catalogs
= null;
202 using (new WriteLock(this._thisLock
))
204 if (!this._isDisposed
)
207 catalogs
= this._catalogCollection
;
208 this._catalogCollection
= null;
209 this._assemblyCatalogs
= null;
210 this._isDisposed
= true;
216 if (catalogs
!= null)
223 this._thisLock
.Dispose();
231 base.Dispose(disposing
);
236 /// Returns the export definitions that match the constraint defined by the specified definition.
238 /// <param name="definition">
239 /// The <see cref="ImportDefinition"/> that defines the conditions of the
240 /// <see cref="ExportDefinition"/> objects to return.
243 /// An <see cref="IEnumerable{T}"/> of <see cref="Tuple{T1, T2}"/> containing the
244 /// <see cref="ExportDefinition"/> objects and their associated
245 /// <see cref="ComposablePartDefinition"/> for objects that match the constraint defined
246 /// by <paramref name="definition"/>.
248 /// <exception cref="ArgumentNullException">
249 /// <paramref name="definition"/> is <see langword="null"/>.
251 /// <exception cref="ObjectDisposedException">
252 /// The <see cref="DirectoryCatalog"/> has been disposed of.
254 public override IEnumerable
<Tuple
<ComposablePartDefinition
, ExportDefinition
>> GetExports(ImportDefinition definition
)
256 this.ThrowIfDisposed();
258 Requires
.NotNull(definition
, "definition");
260 return this._catalogCollection
.SelectMany(catalog
=> catalog
.GetExports(definition
));
264 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changed"/> event.
267 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
269 protected virtual void OnChanged(ComposablePartCatalogChangeEventArgs e
)
271 EventHandler
<ComposablePartCatalogChangeEventArgs
> changedEvent
= this.Changed
;
272 if (changedEvent
!= null)
274 changedEvent(this, e
);
279 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changing"/> event.
282 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
284 protected virtual void OnChanging(ComposablePartCatalogChangeEventArgs e
)
286 EventHandler
<ComposablePartCatalogChangeEventArgs
> changingEvent
= this.Changing
;
287 if (changingEvent
!= null)
289 changingEvent(this, e
);
294 /// Refreshes the <see cref="ComposablePartDefinition"/>s with the latest files in the directory that match
295 /// the searchPattern. If any files have been added they will be added to the catalog and if any files were
296 /// removed they will be removed from the catalog. For files that have been removed keep in mind that the
297 /// assembly cannot be unloaded from the process so <see cref="ComposablePartDefinition"/>s for those files
298 /// will simply be removed from the catalog.
300 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
301 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
303 /// <exception cref="DirectoryNotFoundException">
304 /// The specified <paramref name="path"/> has been removed since object construction.
306 public void Refresh()
308 this.ThrowIfDisposed();
309 Assumes
.NotNull(this._loadedFiles
);
311 List
<Tuple
<string, AssemblyCatalog
>> catalogsToAdd
;
312 List
<Tuple
<string, AssemblyCatalog
>> catalogsToRemove
;
313 ComposablePartDefinition
[] addedDefinitions
;
314 ComposablePartDefinition
[] removedDefinitions
;
315 object changeReferenceObject
;
317 string[] beforeFiles
;
321 afterFiles
= this.GetFiles();
323 using (new ReadLock(this._thisLock
))
325 changeReferenceObject
= this._loadedFiles
;
326 beforeFiles
= this._loadedFiles
.ToArray();
329 this.DiffChanges(beforeFiles
, afterFiles
, out catalogsToAdd
, out catalogsToRemove
);
331 // Don't go any further if there's no work to do
332 if (catalogsToAdd
.Count
== 0 && catalogsToRemove
.Count
== 0)
337 // Notify listeners to give them a preview before completeting the changes
338 addedDefinitions
= catalogsToAdd
339 .SelectMany(cat
=> cat
.Item2
.Parts
)
340 .ToArray
<ComposablePartDefinition
>();
342 removedDefinitions
= catalogsToRemove
343 .SelectMany(cat
=> cat
.Item2
.Parts
)
344 .ToArray
<ComposablePartDefinition
>();
346 using (var atomicComposition
= new AtomicComposition())
348 var changingArgs
= new ComposablePartCatalogChangeEventArgs(addedDefinitions
, removedDefinitions
, atomicComposition
);
349 this.OnChanging(changingArgs
);
351 // if the change went through then write the catalog changes
352 using (new WriteLock(this._thisLock
))
354 if (changeReferenceObject
!= this._loadedFiles
)
356 // Someone updated the list while we were diffing so we need to try the diff again
360 foreach (var catalogToAdd
in catalogsToAdd
)
362 this._assemblyCatalogs
.Add(catalogToAdd
.Item1
, catalogToAdd
.Item2
);
363 this._catalogCollection
.Add(catalogToAdd
.Item2
);
366 foreach (var catalogToRemove
in catalogsToRemove
)
368 this._assemblyCatalogs
.Remove(catalogToRemove
.Item1
);
369 this._catalogCollection
.Remove(catalogToRemove
.Item2
);
372 this._partsQuery
= this._catalogCollection
.AsQueryable().SelectMany(catalog
=> catalog
.Parts
);
373 this._loadedFiles
= afterFiles
.ToReadOnlyCollection();
375 // Lastly complete any changes added to the atomicComposition during the change event
376 atomicComposition
.Complete();
378 // Break out of the while(true)
381 } // AtomicComposition
384 var changedArgs
= new ComposablePartCatalogChangeEventArgs(addedDefinitions
, removedDefinitions
, null);
385 this.OnChanged(changedArgs
);
389 /// Returns a string representation of the directory catalog.
392 /// A <see cref="String"/> containing the string representation of the <see cref="DirectoryCatalog"/>.
394 public override string ToString()
396 return GetDisplayName();
399 private AssemblyCatalog
CreateAssemblyCatalogGuarded(string assemblyFilePath
)
401 Exception exception
= null;
405 return new AssemblyCatalog(assemblyFilePath
, this);
407 catch (FileNotFoundException ex
)
408 { // Files should always exists but don't blow up here if they don't
411 catch (FileLoadException ex
)
412 { // File was found but could not be loaded
415 catch (BadImageFormatException ex
)
416 { // Dlls that contain native code are not loaded, but do not invalidate the Directory
419 catch (ReflectionTypeLoadException ex
)
420 { // Dlls that have missing Managed dependencies are not loaded, but do not invalidate the Directory
424 CompositionTrace
.AssemblyLoadFailed(this, assemblyFilePath
, exception
);
429 private void DiffChanges(string[] beforeFiles
, string[] afterFiles
,
430 out List
<Tuple
<string, AssemblyCatalog
>> catalogsToAdd
,
431 out List
<Tuple
<string, AssemblyCatalog
>> catalogsToRemove
)
433 catalogsToAdd
= new List
<Tuple
<string, AssemblyCatalog
>>();
434 catalogsToRemove
= new List
<Tuple
<string, AssemblyCatalog
>>();
436 IEnumerable
<string> filesToAdd
= afterFiles
.Except(beforeFiles
);
437 foreach (string file
in filesToAdd
)
439 AssemblyCatalog catalog
= CreateAssemblyCatalogGuarded(file
);
443 catalogsToAdd
.Add(new Tuple
<string, AssemblyCatalog
>(file
, catalog
));
447 IEnumerable
<string> filesToRemove
= beforeFiles
.Except(afterFiles
);
448 using (new ReadLock(this._thisLock
))
450 foreach (string file
in filesToRemove
)
452 AssemblyCatalog catalog
;
453 if (this._assemblyCatalogs
.TryGetValue(file
, out catalog
))
455 catalogsToRemove
.Add(new Tuple
<string, AssemblyCatalog
>(file
, catalog
));
461 private string GetDisplayName()
463 return string.Format(CultureInfo
.CurrentCulture
,
464 "{0} (Path=\"{1}\")", // NOLOC
469 private string[] GetFiles()
471 string[] files
= Directory
.GetFiles(this._fullPath
, this._searchPattern
);
472 return Array
.ConvertAll
<string, string>(files
, (file
) => file
.ToUpperInvariant());
475 private static string GetFullPath(string path
)
477 if (!IOPath
.IsPathRooted(path
) && AppDomain
.CurrentDomain
.BaseDirectory
!= null)
479 path
= IOPath
.Combine(AppDomain
.CurrentDomain
.BaseDirectory
, path
);
482 return IOPath
.GetFullPath(path
).ToUpperInvariant();
485 private void Initialize(string path
, string searchPattern
)
488 this._fullPath
= GetFullPath(path
);
489 this._searchPattern
= searchPattern
;
490 this._assemblyCatalogs
= new Dictionary
<string, AssemblyCatalog
>();
491 this._catalogCollection
= new ComposablePartCatalogCollection(null);
493 this._loadedFiles
= GetFiles().ToReadOnlyCollection();
495 foreach (string file
in this._loadedFiles
)
497 AssemblyCatalog assemblyCatalog
= null;
498 assemblyCatalog
= CreateAssemblyCatalogGuarded(file
);
500 if (assemblyCatalog
!= null)
502 this._assemblyCatalogs
.Add(file
, assemblyCatalog
);
503 this._catalogCollection
.Add(assemblyCatalog
);
506 this._partsQuery
= this._catalogCollection
.AsQueryable().SelectMany(catalog
=> catalog
.Parts
);
509 private void ThrowIfDisposed()
511 if (this._isDisposed
)
513 throw ExceptionBuilder
.CreateObjectDisposed(this);
518 /// Gets the display name of the directory catalog.
521 /// A <see cref="String"/> containing a human-readable display name of the <see cref="DirectoryCatalog"/>.
523 [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
524 string ICompositionElement
.DisplayName
526 get { return this.GetDisplayName(); }
530 /// Gets the composition element from which the directory catalog originated.
533 /// This property always returns <see langword="null"/>.
535 [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
536 ICompositionElement ICompositionElement
.Origin