using System.ComponentModel;
using System.Reflection;
using Elsa.Extensions;
using Elsa.Features.Attributes;
using Elsa.Features.Contracts;
using Elsa.Features.Models;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
namespace Elsa.Features.Implementations;
///
public class Module : IModule
{
private sealed record HostedServiceDescriptor(int Order, Type Type);
private Dictionary _features = new();
private readonly HashSet _configuredFeatures = new();
private readonly List _hostedServiceDescriptors = new();
///
/// Constructor.
///
public Module(IServiceCollection services)
{
Services = services;
}
///
public IServiceCollection Services { get; }
///
public IDictionary Properties { get; } = new Dictionary();
///
public bool HasFeature() where T : class, IFeature
{
return HasFeature(typeof(T));
}
///
public bool HasFeature(Type featureType)
{
return _features.ContainsKey(featureType);
}
///
public T Configure(Action? configure = null) where T : class, IFeature
{
return Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure);
}
///
public T Configure(Func factory, Action? configure = null) where T : class, IFeature
{
if (!_features.TryGetValue(typeof(T), out var feature))
{
feature = factory(this);
_features[typeof(T)] = feature;
}
configure?.Invoke((T)feature);
if (!_isApplying)
return (T)feature;
var dependencies = GetDependencyTypes(feature.GetType()).ToHashSet();
foreach (var dependency in dependencies.Select(GetOrCreateFeature))
ConfigureFeature(dependency);
ConfigureFeature(feature);
return (T)feature;
}
///
public IModule ConfigureHostedService(int priority = 0) where T : class, IHostedService
{
return ConfigureHostedService(typeof(T), priority);
}
///
public IModule ConfigureHostedService(Type hostedServiceType, int priority = 0)
{
_hostedServiceDescriptors.Add(new(priority, hostedServiceType));
return this;
}
private bool _isApplying;
///
public void Apply()
{
_isApplying = true;
var featureTypes = GetFeatureTypes();
_features = featureTypes.ToDictionary(featureType => featureType, featureType => _features.TryGetValue(featureType, out var existingFeature) ? existingFeature : (IFeature)Activator.CreateInstance(featureType, this)!);
// Iterate over a copy of the features to avoid concurrent modification exceptions.
foreach (var feature in _features.Values.ToList())
{
// This will cause additional features to be added to _features.
ConfigureFeature(feature);
}
// Filter out features that depend on other features that are not installed.
_features = ExcludeFeaturesWithMissingDependencies(_features.Values).ToDictionary(x => x.GetType(), x => x);
// Hosted services are registered after the features have been applied, because applying a feature can contribute more of them. They still belong
// at this position in the service collection though, ahead of whatever the features register from their Apply method, so remember where that is.
var hostedServiceIndex = Services.Count;
// Make sure to use the complete list of features when applying them. Applying a feature can introduce additional features (a feature calling
// Module.Configure() from its Apply method, directly or through a helper such as AddActivity()), so keep going until nothing new shows up.
var appliedFeatures = new HashSet();
while (GetFeaturesPendingApply(appliedFeatures) is { Count: > 0 } pendingFeatures)
{
appliedFeatures.UnionWith(pendingFeatures);
foreach (var feature in pendingFeatures)
feature.Apply();
}
// Add hosted services in order of priority.
RegisterHostedServices(hostedServiceIndex);
// Add a registry of enabled features to the service collection for client applications to reflect on what features are installed.
var registry = new InstalledFeatureRegistry();
foreach (var feature in _features.Values)
{
var type = feature.GetType();
var name = type.Name.Replace("Feature", string.Empty);
var ns = "Elsa";
var displayName = type.GetCustomAttribute()?.DisplayName ?? name;
var description = type.GetCustomAttribute()?.Description;
registry.Add(new(name, ns, displayName, description));
}
Services.AddSingleton(registry);
Services.AddSingleton(sp => new InstalledFeatureProvider(sp.GetRequiredService()));
}
private IEnumerable ExcludeFeaturesWithMissingDependencies(IEnumerable features)
{
return
from feature in features
let featureType = feature.GetType()
let dependencyOfAttributes = featureType.GetCustomAttributes(true).ToList()
let missingDependencies = dependencyOfAttributes.Where(x => !_features.ContainsKey(x.Type)).ToList()
where missingDependencies.Count == 0
select feature;
}
// Registers the configured hosted services in order of priority, starting at the specified index.
private void RegisterHostedServices(int index)
{
var appendIndex = Services.Count;
foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.Type));
// TryAddEnumerable appends, so move what it added back to where hosted services configured through this module belong. Doing it this way rather
// than inserting directly keeps the de-duplication behaviour of TryAddEnumerable, which also considers what the features registered themselves.
var appendedDescriptors = Services.Skip(appendIndex).ToList();
for (var i = 0; i < appendedDescriptors.Count; i++)
Services.RemoveAt(appendIndex);
for (var i = 0; i < appendedDescriptors.Count; i++)
Services.Insert(index + i, appendedDescriptors[i]);
}
// Returns the features that have not been applied yet, sorted so that dependencies are applied before the features that depend on them.
private List GetFeaturesPendingApply(IReadOnlySet appliedFeatures)
{
var pendingFeatureTypes = _features.Where(x => !appliedFeatures.Contains(x.Value)).Select(x => x.Key).ToList();
var pendingFeatureTypeLookup = pendingFeatureTypes.ToHashSet();
// Sorting pulls in dependencies that are not pending themselves, so filter those out again.
return pendingFeatureTypes
.TSort(GetDeclaredDependencyTypes)
.Where(pendingFeatureTypeLookup.Contains)
.Select(x => _features[x])
.Distinct()
.ToList();
}
private void ConfigureFeature(IFeature feature)
{
if (_configuredFeatures.Contains(feature))
return;
feature.Configure();
feature.ConfigureHostedServices();
_features[feature.GetType()] = feature;
_configuredFeatures.Add(feature);
}
private IFeature GetOrCreateFeature(Type featureType)
{
return _features.TryGetValue(featureType, out var existingFeature) ? existingFeature : (IFeature)Activator.CreateInstance(featureType, this)!;
}
private HashSet GetFeatureTypes()
{
var featureTypes = _features.Keys.ToHashSet();
var featureTypesWithDependencies = featureTypes.Concat(featureTypes.SelectMany(GetDependencyTypes)).ToHashSet();
return featureTypesWithDependencies.TSort(GetDeclaredDependencyTypes).ToHashSet();
}
private static IEnumerable GetDeclaredDependencyTypes(Type type)
{
return type.GetCustomAttributes(true).Select(dependsOn => dependsOn.Type);
}
// Recursively get dependency types.
private IEnumerable GetDependencyTypes(Type type)
{
var dependencies = GetDeclaredDependencyTypes(type).ToList();
return dependencies.Concat(dependencies.SelectMany(GetDependencyTypes));
}
}