Skip to content

Commit

Permalink
Added an IInitializer interface, exposed the ClassCatalog through the…
Browse files Browse the repository at this point in the history
… engine, and changed Razor to use the default service collection
  • Loading branch information
daveaglick committed Mar 27, 2020
1 parent 697731b commit 7ac71a3
Show file tree
Hide file tree
Showing 24 changed files with 375 additions and 188 deletions.
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 1.0.0-alpha.29

- Added a new `IInitializer` interface that can be used for library/module initialization (but only when using the `Bootstrapper`).
- Refactored the `RenderRazor` module to use the built-in service collection when possible.
- Added a new `IServiceCollection.AddRazor()` extension to register Razor services out-of-band.
- Refactored the `ClassCatalog` to `Statiq.Common` and exposed it via the `IExecutionState` interface.
- Added `.WithSource()` to the `PaginateDocuments` and `GroupDocuments` modules.
- Added a new `Keys.Order` key and made the `OrderDocuments` module support it.
- Added a `keepExisting` parameter to the `GenerateExcerpt` module.
Expand Down
8 changes: 3 additions & 5 deletions src/core/Statiq.App/Bootstrapper/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ namespace Statiq.App
{
public class Bootstrapper : IConfigurableBootstrapper
{
private readonly ClassCatalog _classCatalog = new ClassCatalog();

private Func<CommandServiceTypeRegistrar, ICommandApp> _getCommandApp = x => new CommandApp(x);

// Private constructor to force factory use which returns the interface to get access to default interface implementations
Expand All @@ -26,7 +24,7 @@ internal Bootstrapper(string[] arguments)
}

/// <inheritdoc/>
public IClassCatalog ClassCatalog => _classCatalog;
public ClassCatalog ClassCatalog { get; } = new ClassCatalog();

/// <inheritdoc/>
public IConfiguratorCollection Configurators { get; } = new ConfiguratorCollection();
Expand All @@ -49,7 +47,7 @@ public async Task<int> RunAsync()
await default(SynchronizationContextRemover);

// Populate the class catalog (if we haven't already)
_classCatalog.Populate();
ClassCatalog.Populate();

// Run bootstrapper configurators first
Configurators.Configure<IConfigurableBootstrapper>(this);
Expand All @@ -66,7 +64,7 @@ public async Task<int> RunAsync()
IServiceCollection serviceCollection = CreateServiceCollection() ?? new ServiceCollection();
serviceCollection.TryAddSingleton(this);
serviceCollection.TryAddSingleton<IConfigurableBootstrapper>(this);
serviceCollection.TryAddSingleton(_classCatalog); // The class catalog is retrieved later for deferred logging once a service provider is built
serviceCollection.TryAddSingleton(ClassCatalog); // The class catalog is retrieved later for deferred logging once a service provider is built
serviceCollection.TryAddSingleton<IConfiguration>(configurationRoot);

// Run configurators on the service collection
Expand Down
2 changes: 1 addition & 1 deletion src/core/Statiq.App/Commands/BaseCommand{TSettings}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public sealed override async Task<int> ExecuteAsync(CommandContext context, TSet
// Log pending messages
ILogger logger = serviceScope.ServiceProvider.GetRequiredService<ILogger<Bootstrapper>>();
logger.LogInformation($"Statiq version {Engine.Version}");
classCatalog?.LogDebugMessages(logger);
classCatalog?.LogDebugMessagesTo(logger);

// Attach
if (commandSettings.Attach)
Expand Down
2 changes: 1 addition & 1 deletion src/core/Statiq.App/Commands/EngineManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public EngineManager(

// Create the engine and get a logger
IDictionary<string, object> configurationOverrides = (configurationSettings as ConfigurationSettings)?.Dictionary;
Engine = new Engine(applicationState, serviceCollection, configurationSettings.Configuration, configurationOverrides);
Engine = new Engine(applicationState, serviceCollection, configurationSettings.Configuration, configurationOverrides, bootstrapper.ClassCatalog);
_logger = Engine.Services.GetRequiredService<ILogger<Bootstrapper>>();

// Apply command settings
Expand Down
1 change: 0 additions & 1 deletion src/core/Statiq.App/Statiq.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" />
<PackageReference Include="NetEscapades.Extensions.Logging.RollingFile" Version="2.2.0" />
Expand Down
62 changes: 0 additions & 62 deletions src/core/Statiq.Common/Configuration/IClassCatalog.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
public interface IConfigurableBootstrapper : IConfigurable
{
/// <summary>
/// A catalog of all classes in all assemblies loaded by the current
/// application context.
/// A catalog of all classes in all assemblies loaded by the current application context.
/// </summary>
IClassCatalog ClassCatalog { get; }
ClassCatalog ClassCatalog { get; }

/// <summary>
/// A collection of all configurators to be run on the bootstrapper.
Expand Down
10 changes: 10 additions & 0 deletions src/core/Statiq.Common/Configuration/IInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Statiq.Common
{
/// <summary>
/// Implement this interface to define an initializer that will get automatically
/// instantiated and run by the bootstrapper at startup.
/// </summary>
public interface IInitializer : IConfigurator<IConfigurableBootstrapper>
{
}
}
5 changes: 5 additions & 0 deletions src/core/Statiq.Common/Execution/IExecutionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public interface IExecutionState : IDocumentFactory
/// </summary>
IReadOnlyApplicationState ApplicationState { get; }

/// <summary>
/// A catalog of all classes in all assemblies loaded by the current application context.
/// </summary>
ClassCatalog ClassCatalog { get; }

/// <summary>
/// Indicates that the engine is executing pipeline phases and modules in serial.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Reflection;

namespace Statiq.App
namespace Statiq.Common
{
/// <summary>
/// Compares two assemblies for equality by comparing at their full names.
/// </summary>
internal class AssemblyComparer : IEqualityComparer<Assembly>
public class AssemblyComparer : IEqualityComparer<Assembly>
{
/// <inheritdoc/>
public bool Equals(Assembly x, Assembly y) => x?.FullName.Equals(y?.FullName) ?? false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,92 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.Logging;
using Statiq.Common;

namespace Statiq.App
namespace Statiq.Common
{
/// <summary>
/// Responsible for iterating over a set of assemblies
/// looking for implementations of predefined interfaces.
/// </summary>
internal class ClassCatalog : IClassCatalog
public class ClassCatalog
{
private readonly ConcurrentDictionary<string, Type> _types = new ConcurrentDictionary<string, Type>();

private readonly ConcurrentQueue<string> _debugMessages = new ConcurrentQueue<string>();

private readonly object _populateLock = new object();

private bool _populated;
private Assembly[] _assemblies;

/// <inheritdoc/>
/// <summary>
/// Gets all loaded assemblies.
/// </summary>
/// <returns>A collection of available assemblies.</returns>
public IReadOnlyList<Assembly> GetAssemblies()
{
Populate();
return _assemblies;
}

/// <summary>
/// Gets all types assignable to a specified type.
/// </summary>
/// <param name="assignableType">The type of classes to get.</param>
/// <returns>All classes of the specified type.</returns>
public IEnumerable<Type> GetTypesAssignableTo(Type assignableType)
{
_ = assignableType ?? throw new ArgumentNullException(nameof(assignableType));
Populate();
return _types.Values.Where(x => assignableType.IsAssignableFrom(x));
}

/// <inheritdoc/>
/// <summary>
/// Gets all types from a specified assembly.
/// </summary>
/// <param name="assembly">The assembly to get types from.</param>
/// <returns>All types from the specified assembly.</returns>
public IEnumerable<Type> GetTypesFromAssembly(Assembly assembly)
{
_ = assembly ?? throw new ArgumentNullException(nameof(assembly));
Populate();
return _types.Values.Where(x => x.Assembly.Equals(assembly));
}

/// <inheritdoc/>
/// <summary>
/// Gets instances for all classes of a specified assignable type.
/// </summary>
/// <param name="assignableType">The type of instances to get.</param>
/// <returns>Instances for all classes of the specified type.</returns>
public IEnumerable<object> GetInstances(Type assignableType)
{
_ = assignableType ?? throw new ArgumentNullException(nameof(assignableType));
Populate();
return GetTypesAssignableTo(assignableType).Select(Activator.CreateInstance);
}

/// <inheritdoc/>
/// <summary>
/// Gets a type for the specified full name.
/// </summary>
/// <param name="fullName">The full name of the type.</param>
/// <returns>
/// A <see cref="Type"/> that matches the specified full name or <c>null</c>
/// if a corresponding type could not be found.
/// </returns>
public Type GetType(string fullName)
{
_ = fullName ?? throw new ArgumentNullException(nameof(fullName));
Populate();
return _types.TryGetValue(fullName, out Type type) ? type : default;
}

/// <summary>
/// Gets an instance for a specified full name.
/// </summary>
/// <param name="fullName">The full name of the type.</param>
/// <returns>
/// An instance of the type that matches the full name or <c>null</c>
/// if a corresponding type could not be found.
/// </returns>
public object GetInstance(string fullName)
{
_ = fullName ?? throw new ArgumentNullException(nameof(fullName));
Expand All @@ -65,7 +101,16 @@ public object GetInstance(string fullName)
: default;
}

/// <inheritdoc/>
/// <summary>
/// Gets an instance for a class of a specified assignable type and name.
/// </summary>
/// <param name="assignableType">The assignable type of instance to get.</param>
/// <param name="typeName">The name of the type.</param>
/// <param name="ignoreCase">if set to <c>true</c> ignore the case of the type name.</param>
/// <returns>
/// An instance of the first class that matches the specified type and name or <c>null</c>
/// if a corresponding type could not be found.
/// </returns>
public object GetInstance(Type assignableType, string typeName, bool ignoreCase = false)
{
_ = assignableType ?? throw new ArgumentNullException(nameof(assignableType));
Expand All @@ -76,7 +121,7 @@ public object GetInstance(Type assignableType, string typeName, bool ignoreCase
return type == null ? default : Activator.CreateInstance(type);
}

internal void LogDebugMessages(ILogger logger)
public void LogDebugMessagesTo(ILogger logger)
{
if (logger != null)
{
Expand All @@ -87,31 +132,38 @@ internal void LogDebugMessages(ILogger logger)
}
}

/// <summary>
/// Populates the catalog. Population will only occur once and if the catalog has already been
/// populated this method will immediatly return.
/// </summary>
public void Populate()
{
lock (_populateLock)
{
if (!_populated)
if (_assemblies == null)
{
Assembly[] assemblies;
// Add (and load) all assembly dependencies
HashSet<Assembly> assemblies;
try
{
assemblies = DependencyContext.Default.RuntimeLibraries
.SelectMany(library => library.GetDefaultAssemblyNames(DependencyContext.Default))
.Select(Assembly.Load)
.Distinct(new AssemblyComparer())
.ToArray();
assemblies = new HashSet<Assembly>(
DependencyContext.Default.RuntimeLibraries
.SelectMany(library => library.GetDefaultAssemblyNames(DependencyContext.Default))
.Select(Assembly.Load),
new AssemblyComparer());
}
catch
{
// The DependencyContext may not be available on all platforms so fall
// back to recursively loading all referenced assemblies of the entry assembly
HashSet<Assembly> visited = new HashSet<Assembly>(new AssemblyComparer());
LoadAssemblies(Assembly.GetEntryAssembly(), visited);
assemblies = visited.ToArray();
assemblies = new HashSet<Assembly>(new AssemblyComparer());
LoadAssemblies(Assembly.GetEntryAssembly(), assemblies);
}

// Load types
// Make sure we've also got all assemblies from the current domain
assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies());

// Load types in parallel
Parallel.ForEach(assemblies, assembly =>
{
_debugMessages.Enqueue($"Cataloging types in assembly {assembly.FullName}");
Expand All @@ -120,8 +172,9 @@ public void Populate()
_types.TryAdd(type.FullName, type);
}
});

_assemblies = assemblies.ToArray();
}
_populated = true;
}
}

Expand Down
Loading

0 comments on commit 7ac71a3

Please sign in to comment.