< Summary

Information
Class: Elsa.Features.Implementations.Module
Assembly: Elsa.Features
File(s): /home/runner/work/elsa-core/elsa-core/src/common/Elsa.Features/Implementations/Module.cs
Line coverage
97%
Covered lines: 86
Uncovered lines: 2
Coverable lines: 88
Total lines: 222
Line coverage: 97.7%
Branch coverage
94%
Covered branches: 34
Total branches: 36
Branch coverage: 94.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Order()100%11100%
.ctor(...)100%11100%
get_Services()100%11100%
get_Properties()100%11100%
HasFeature()100%210%
HasFeature(...)100%210%
Configure(...)100%22100%
Configure(...)100%88100%
ConfigureHostedService(...)100%11100%
ConfigureHostedService(...)100%11100%
Apply()87.5%1616100%
ExcludeFeaturesWithMissingDependencies(...)100%11100%
RegisterHostedServices(...)100%66100%
GetFeaturesPendingApply(...)100%11100%
ConfigureFeature(...)100%22100%
GetOrCreateFeature(...)100%22100%
GetFeatureTypes()100%11100%
GetDeclaredDependencyTypes(...)100%11100%
GetDependencyTypes(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/common/Elsa.Features/Implementations/Module.cs

#LineLine coverage
 1using System.ComponentModel;
 2using System.Reflection;
 3using Elsa.Extensions;
 4using Elsa.Features.Attributes;
 5using Elsa.Features.Contracts;
 6using Elsa.Features.Models;
 7using Elsa.Features.Services;
 8using Microsoft.Extensions.DependencyInjection;
 9using Microsoft.Extensions.DependencyInjection.Extensions;
 10using Microsoft.Extensions.Hosting;
 11
 12namespace Elsa.Features.Implementations;
 13
 14/// <inheritdoc />
 15public class Module : IModule
 16{
 6617    private sealed record HostedServiceDescriptor(int Order, Type Type);
 18
 1319    private Dictionary<Type, IFeature> _features = new();
 1320    private readonly HashSet<IFeature> _configuredFeatures = new();
 1321    private readonly List<HostedServiceDescriptor> _hostedServiceDescriptors = new();
 22
 23    /// <summary>
 24    /// Constructor.
 25    /// </summary>
 1326    public Module(IServiceCollection services)
 27    {
 1328        Services = services;
 1329    }
 30
 31    /// <inheritdoc />
 82232    public IServiceCollection Services { get; }
 33
 34    /// <inheritdoc />
 6835    public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
 36
 37    /// <inheritdoc />
 38    public bool HasFeature<T>() where T : class, IFeature
 39    {
 040        return HasFeature(typeof(T));
 41    }
 42
 43    /// <inheritdoc />
 44    public bool HasFeature(Type featureType)
 45    {
 046        return _features.ContainsKey(featureType);
 47    }
 48
 49    /// <inheritdoc />
 50    public T Configure<T>(Action<T>? configure = null) where T : class, IFeature
 51    {
 33852        return Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure);
 53    }
 54
 55    /// <inheritdoc />
 56    public T Configure<T>(Func<IModule, T> factory, Action<T>? configure = null) where T : class, IFeature
 57    {
 23858        if (!_features.TryGetValue(typeof(T), out var feature))
 59        {
 10060            feature = factory(this);
 10061            _features[typeof(T)] = feature;
 62        }
 63
 23864        configure?.Invoke((T)feature);
 65
 23866        if (!_isApplying)
 1367            return (T)feature;
 68
 22569        var dependencies = GetDependencyTypes(feature.GetType()).ToHashSet();
 296670        foreach (var dependency in dependencies.Select(GetOrCreateFeature))
 125871            ConfigureFeature(dependency);
 72
 22573        ConfigureFeature(feature);
 22574        return (T)feature;
 75    }
 76
 77    /// <inheritdoc />
 78    public IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService
 79    {
 2280        return ConfigureHostedService(typeof(T), priority);
 81    }
 82
 83    /// <inheritdoc />
 84    public IModule ConfigureHostedService(Type hostedServiceType, int priority = 0)
 85    {
 2286        _hostedServiceDescriptors.Add(new(priority, hostedServiceType));
 2287        return this;
 88    }
 89
 90    private bool _isApplying;
 91
 92    /// <inheritdoc />
 93    public void Apply()
 94    {
 1395        _isApplying = true;
 1396        var featureTypes = GetFeatureTypes();
 13797        _features = featureTypes.ToDictionary(featureType => featureType, featureType => _features.TryGetValue(featureTy
 98
 99        // Iterate over a copy of the features to avoid concurrent modification exceptions.
 150100        foreach (var feature in _features.Values.ToList())
 101        {
 102            // This will cause additional features to be added to _features.
 62103            ConfigureFeature(feature);
 104        }
 105
 106        // Filter out features that depend on other features that are not installed.
 317107        _features = ExcludeFeaturesWithMissingDependencies(_features.Values).ToDictionary(x => x.GetType(), x => x);
 108
 109        // Hosted services are registered after the features have been applied, because applying a feature can contribut
 110        // at this position in the service collection though, ahead of whatever the features register from their Apply m
 13111        var hostedServiceIndex = Services.Count;
 112
 113        // Make sure to use the complete list of features when applying them. Applying a feature can introduce additiona
 114        // Module.Configure<T>() from its Apply method, directly or through a helper such as AddActivity<T>()), so keep 
 13115        var appliedFeatures = new HashSet<IFeature>();
 35116        while (GetFeaturesPendingApply(appliedFeatures) is { Count: > 0 } pendingFeatures)
 117        {
 22118            appliedFeatures.UnionWith(pendingFeatures);
 119
 368120            foreach (var feature in pendingFeatures)
 162121                feature.Apply();
 122        }
 123
 124        // Add hosted services in order of priority.
 13125        RegisterHostedServices(hostedServiceIndex);
 126
 127        // Add a registry of enabled features to the service collection for client applications to reflect on what featu
 13128        var registry = new InstalledFeatureRegistry();
 350129        foreach (var feature in _features.Values)
 130        {
 162131            var type = feature.GetType();
 162132            var name = type.Name.Replace("Feature", string.Empty);
 162133            var ns = "Elsa";
 162134            var displayName = type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? name;
 162135            var description = type.GetCustomAttribute<DescriptionAttribute>()?.Description;
 162136            registry.Add(new(name, ns, displayName, description));
 137        }
 138
 13139        Services.AddSingleton<IInstalledFeatureRegistry>(registry);
 16140        Services.AddSingleton<IInstalledFeatureProvider>(sp => new InstalledFeatureProvider(sp.GetRequiredService<IInsta
 13141    }
 142
 143    private IEnumerable<IFeature> ExcludeFeaturesWithMissingDependencies(IEnumerable<IFeature> features)
 144    {
 13145        return
 13146            from feature in features
 152147            let featureType = feature.GetType()
 152148            let dependencyOfAttributes = featureType.GetCustomAttributes<DependencyOfAttribute>(true).ToList()
 161149            let missingDependencies = dependencyOfAttributes.Where(x => !_features.ContainsKey(x.Type)).ToList()
 152150            where missingDependencies.Count == 0
 165151            select feature;
 152    }
 153
 154    // Registers the configured hosted services in order of priority, starting at the specified index.
 155    private void RegisterHostedServices(int index)
 156    {
 13157        var appendIndex = Services.Count;
 158
 92159        foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
 22160            Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.Type))
 161
 162        // TryAddEnumerable appends, so move what it added back to where hosted services configured through this module 
 163        // than inserting directly keeps the de-duplication behaviour of TryAddEnumerable, which also considers what the
 13164        var appendedDescriptors = Services.Skip(appendIndex).ToList();
 165
 70166        for (var i = 0; i < appendedDescriptors.Count; i++)
 22167            Services.RemoveAt(appendIndex);
 168
 70169        for (var i = 0; i < appendedDescriptors.Count; i++)
 22170            Services.Insert(index + i, appendedDescriptors[i]);
 13171    }
 172
 173    // Returns the features that have not been applied yet, sorted so that dependencies are applied before the features 
 174    private List<IFeature> GetFeaturesPendingApply(IReadOnlySet<IFeature> appliedFeatures)
 175    {
 531176        var pendingFeatureTypes = _features.Where(x => !appliedFeatures.Contains(x.Value)).Select(x => x.Key).ToList();
 35177        var pendingFeatureTypeLookup = pendingFeatureTypes.ToHashSet();
 178
 179        // Sorting pulls in dependencies that are not pending themselves, so filter those out again.
 35180        return pendingFeatureTypes
 35181            .TSort(GetDeclaredDependencyTypes)
 35182            .Where(pendingFeatureTypeLookup.Contains)
 162183            .Select(x => _features[x])
 35184            .Distinct()
 35185            .ToList();
 186    }
 187
 188    private void ConfigureFeature(IFeature feature)
 189    {
 1545190        if (_configuredFeatures.Contains(feature))
 1383191            return;
 192
 162193        feature.Configure();
 162194        feature.ConfigureHostedServices();
 162195        _features[feature.GetType()] = feature;
 162196        _configuredFeatures.Add(feature);
 162197    }
 198
 199    private IFeature GetOrCreateFeature(Type featureType)
 200    {
 1258201        return _features.TryGetValue(featureType, out var existingFeature) ? existingFeature : (IFeature)Activator.Creat
 202    }
 203
 204    private HashSet<Type> GetFeatureTypes()
 205    {
 13206        var featureTypes = _features.Keys.ToHashSet();
 13207        var featureTypesWithDependencies = featureTypes.Concat(featureTypes.SelectMany(GetDependencyTypes)).ToHashSet();
 13208        return featureTypesWithDependencies.TSort(GetDeclaredDependencyTypes).ToHashSet();
 209    }
 210
 211    private static IEnumerable<Type> GetDeclaredDependencyTypes(Type type)
 212    {
 4051213        return type.GetCustomAttributes<DependsOnAttribute>(true).Select(dependsOn => dependsOn.Type);
 214    }
 215
 216    // Recursively get dependency types.
 217    private IEnumerable<Type> GetDependencyTypes(Type type)
 218    {
 1908219        var dependencies = GetDeclaredDependencyTypes(type).ToList();
 1908220        return dependencies.Concat(dependencies.SelectMany(GetDependencyTypes));
 221    }
 222}