Skip to content

Commit

Permalink
allow plugins to expose IEnumerable<> or IAsyncEnumerable<> of compon…
Browse files Browse the repository at this point in the history
…ent types
  • Loading branch information
IS4Code committed Apr 28, 2023
1 parent 598f93b commit 1a433da
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 46 deletions.
105 changes: 89 additions & 16 deletions SFI.Application/ComponentCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ public static ComponentCollection Create(Type elementType, IEnumerable collectio
/// <param name="component">The component type to create an instance of.</param>
/// <param name="resultFactory">A factory object receiving the created instance.</param>
/// <param name="args">Arguments of <paramref name="resultFactory"/>.</param>
/// <returns>The result of <paramref name="resultFactory"/>.</returns>
public abstract ValueTask<TResult?> CreateInstance<TResult, TArgs>(ComponentType component, IResultFactory<TResult, TArgs> resultFactory, TArgs args);
/// <returns>The results of <paramref name="resultFactory"/>.</returns>
public abstract IAsyncEnumerable<TResult?> CreateInstance<TResult, TArgs>(ComponentType component, IResultFactory<TResult, TArgs> resultFactory, TArgs args);

/// <summary>
/// Invokes <paramref name="resultFactory"/> over each element in the collection.
Expand Down Expand Up @@ -124,33 +124,106 @@ public ComponentCollection(ICollection<T> collection, ComponentCollectionAttribu
}

/// <inheritdoc/>
public override async ValueTask<TResult?> CreateInstance<TResult, TArgs>(ComponentType component, IResultFactory<TResult, TArgs> resultFactory, TArgs args) where TResult : default
public override async IAsyncEnumerable<TResult?> CreateInstance<TResult, TArgs>(ComponentType component, IResultFactory<TResult, TArgs> resultFactory, TArgs args) where TResult : default
{
var type = Attribute.CommonType;
if(type != null)
// Check that a type produced from the component can be added to the collection
if(MatchType(component.Type, true).Any(elementType.IsAssignableFrom))
{
if(type.IsGenericTypeDefinition)
var inst = component.GetInstance();
if(inst is T instance)
{
if(!component.Type.GetInterfaces().Any(i => i.IsGenericType && type.Equals(i.GetGenericTypeDefinition())))
yield return await resultFactory.Invoke(instance, args);
}
if(inst is IAsyncEnumerable<T> asyncEnumerable)
{
await foreach(var obj in asyncEnumerable)
{
return default;
yield return await resultFactory.Invoke(obj, args);
}
}else{
if(!type.IsAssignableFrom(component.Type))
}else if(inst is IEnumerable<T> enumerable)
{
foreach(var obj in enumerable)
{
return default;
yield return await resultFactory.Invoke(obj, args);
}
}
}
if(!elementType.IsAssignableFrom(component.Type))
}

static readonly Type enumerableType = typeof(IEnumerable<>);
static readonly Type asyncEnumerableType = typeof(IAsyncEnumerable<>);

/// <summary>
/// Checks whether <paramref name="componentType"/> matches the type desired by this
/// collection, directly and through implementation of <see cref="IEnumerable{T}"/>
/// or <see cref="IAsyncEnumerable{T}"/>.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="allowCollections">Whether to allow collection types.</param>
/// <returns>The sequence of all types returned from the component.</returns>
IEnumerable<Type> MatchType(Type componentType, bool allowCollections)
{
var commonType = Attribute.CommonType;
if(commonType != null)
{
return default;
if(!commonType.IsGenericTypeDefinition)
{
// Type is directly assignable
if(commonType.IsAssignableFrom(componentType))
{
yield return componentType;
}
// Continues below
}else{
foreach(var i in componentType.GetInterfaces())
{
if(i.IsGenericType)
{
var def = i.GetGenericTypeDefinition();
// The interface definition matches the collection type
if(commonType.Equals(def))
{
yield return componentType;
if(!allowCollections)
{
// No need to check further
break;
}
}
// Top-level interface is IEnumerable or IAsyncEnumerable
if(allowCollections && (enumerableType.Equals(def) || asyncEnumerableType.Equals(def)))
{
foreach(var type in MatchType(i.GetGenericArguments()[0], false))
{
yield return type;
}
}
}
}
yield break;
}
}else{
// Automatically available
yield return componentType;
}
if(component.GetInstance() is T obj)
if(allowCollections)
{
return await resultFactory.Invoke(obj, args);
// It implements IEnumerable or IAsyncEnumerable of a matching type (only top-level)
foreach(var i in componentType.GetInterfaces())
{
if(i.IsGenericType)
{
var def = i.GetGenericTypeDefinition();
if(enumerableType.Equals(def) || asyncEnumerableType.Equals(def))
{
foreach(var type in MatchType(i.GetGenericArguments()[0], false))
{
yield return type;
}
}
}
}
}
return default;
}

/// <inheritdoc/>
Expand Down
35 changes: 14 additions & 21 deletions SFI.Application/ComponentType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,16 @@ namespace IS4.SFI.Application
public class ComponentType
{
readonly ComponentInspector inspector;
readonly Func<object> factory;

/// <summary>
/// The type of the component.
/// </summary>
public Type Type { get; }

/// <summary>
/// <see langword="true"/> if an error occurred during the creation of the object.
/// The instance of the component, or error.
/// </summary>
public bool Error { get; private set; }

/// <summary>
/// The instance of the component, if previously created.
/// </summary>
public object? Instance { get; private set; }
readonly Lazy<(bool success, object result)> instance;

/// <summary>
/// Creates a new instance of the class.
Expand All @@ -34,8 +28,17 @@ public class ComponentType
public ComponentType(Type type, Func<object> factory, ComponentInspector inspector)
{
this.inspector = inspector;
this.factory = factory;
Type = type;

instance = new(() => {
try{
return (true, factory());
}catch(Exception e) when(GlobalOptions.SuppressNonCriticalExceptions)
{
inspector?.OutputLog?.WriteLine($"An exception occurred while creating an instance of type {Type} from assembly {Type.Assembly.GetName().Name}: {e}");
return (false, e);
}
}, false);
}

/// <summary>
Expand All @@ -45,18 +48,8 @@ public ComponentType(Type type, Func<object> factory, ComponentInspector inspect
public object? GetInstance()
{
// Re-use instance if already created
if(Instance == null && !Error)
{
try{
Instance = factory();
}catch(Exception e) when(GlobalOptions.SuppressNonCriticalExceptions)
{
inspector?.OutputLog?.WriteLine($"An exception occurred while creating an instance of type {Type} from assembly {Type.Assembly.GetName().Name}: {e}");
// Prevents attempting to create the instance next time
Error = true;
}
}
return Instance;
var (success, value) = instance.Value;
return success ? value : null;
}
}
}
25 changes: 16 additions & 9 deletions SFI.Application/ExtensibleInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace IS4.SFI
/// <summary>
/// An implementation of <see cref="Inspector"/> allowing loading of plugins.
/// </summary>
public abstract class ExtensibleInspector : ComponentInspector, IResultFactory<bool, IEnumerable>
public abstract class ExtensibleInspector : ComponentInspector, IResultFactory<object?, IEnumerable>
{
/// <summary>
/// Contains the collection of plugins loaded by the inspector.
Expand All @@ -45,21 +45,28 @@ async ValueTask LoadPlugins()

foreach(var component in LoadPlugin(plugin.Directory, plugin.MainFile))
{
int componentCount = 0;

foreach(var collection in ComponentCollections)
{
await collection.CreateInstance(component, this, collection.Collection);
await foreach(var instance in collection.CreateInstance(component, this, collection.Collection))
{
if(instance != null)
{
componentCount++;
CaptureCollections(instance);
}
}
}

if(component.Instance != null)
if(componentCount > 0)
{
var assembly = component.Type.Assembly;
if(!loaded.TryGetValue(assembly, out var count))
{
count = 0;
}
loaded[assembly] = count + 1;

CaptureCollections(component.Instance);
loaded[assembly] = count + componentCount;
}
}
}
Expand All @@ -73,14 +80,14 @@ async ValueTask LoadPlugins()
}
}

async ITask<bool> IResultFactory<bool, IEnumerable>.Invoke<T>(T value, IEnumerable sequence)
async ITask<object?> IResultFactory<object?, IEnumerable>.Invoke<T>(T value, IEnumerable sequence)
{
if(sequence is ICollection<T> collection)
{
collection.Add(value);
return true;
return value;
}
return false;
return null;
}

/// <summary>
Expand Down

0 comments on commit 1a433da

Please sign in to comment.