Sebastian Schumann
Sebastian Schumann

Reputation: 3446

MEF vs. IAsyncDisposable or iterate over all instantiated exports

I've shared MEF exports that implement IAsyncDisposable. If an export in MEF implements IDisposable it will be disposed when the composition container (or maybe the catalog) is disposed.

IAsyncDisposable is not recognized from MEF. Is there any solution to that problem?

If not: If the application will be shut down I try to iterate over all already created exports that implement IAsyncDisposable but there seems to be no possibility to do that.

Let's assume that I've a typed method (maybe created via reflection) I'm able to call CompositionContainer.GetExport<T>() that returns a Lazy<T>. The problem is that IsValueCreated is false - even if that import has a running shared instance.

Is there any way to iterate over all exports that have been already instantiated?

This code shows that the Lazy<T>-instance does not contain the already known exported value:

public class Program
{
    public static void Main()
    {
        using var catalog = new AssemblyCatalog(typeof(Program).Assembly);
        using var container = new CompositionContainer(catalog);
        
        var dummyInstance = container.GetExport<Foo>().Value;
        Console.WriteLine($"HasInstance: {dummyInstance is not null}");
        
        var export = container.GetExport<Foo>();
        Console.WriteLine($"IsValueCreated: {export.IsValueCreated}");
    }
}

[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
public class Foo 
{
}

The output is:

HasInstance: True
IsValueCreated: False

DEMO

EDIT

I found a "solution" to that problem using a lot of reflection and magic field names:

var catalogExportProviderProperty = compositionContainer
                                    .GetType()
                                    .GetProperty("CatalogExportProvider", BindingFlags.Instance | BindingFlags.NonPublic) ??
                                    throw ReflectionErrors.MissingProperty(
                                        compositionContainer.GetType(),
                                        "CatalogExportProvider");
var catalogExportProvider =
    catalogExportProviderProperty.GetValue(compositionContainer) ??
    throw new InvalidOperationException(
        $@"Uninitialized property 'CatalogExportProvider' in {compositionContainer.GetType().Name}.");
var partsToDisposeField =
    catalogExportProvider.GetType().GetField("_partsToDispose", BindingFlags.Instance | BindingFlags.NonPublic) ??
    throw ReflectionErrors.MissingField(catalogExportProvider.GetType(), "_partsToDispose");
var partsToDispose = partsToDisposeField.GetValue(catalogExportProvider) as IEnumerable ??
                     throw new InvalidOperationException($@"Unable to retrieve disposable parts from {catalogExportProvider.GetType()}.");
foreach (var item in partsToDispose.OfType<object>())
{
    var cachedInstanceProperty = item.GetType().GetProperty("CachedInstance", BindingFlags.Instance | BindingFlags.NonPublic) ??
                                 throw ReflectionErrors.MissingProperty(item.GetType(), "CachedInstance");
    var cachedInstance = cachedInstanceProperty.GetValue(item, index: null);
    if (cachedInstance is IAsyncDisposable asyncDisposable)
    {
        await asyncDisposable.DisposeAsync();
    }
}

This works because there is an implementation specification that every type that implements IAsyncDisposable should also implement IDisposable.

BUT: This solution doesn't feel right. I'm still looking for an official solution to that problem.

Upvotes: 0

Views: 87

Answers (1)

Sebastian Schumann
Sebastian Schumann

Reputation: 3446

I found a solution by implementing a CustomCompositionContainer.

The following points were taken into account:

Export

  • In this class, the instances are retrieved via the GetExportedValueCore method, which can be overwritten.
  • A wrapper is created that overwrites the above-mentioned method and provides information via a callback.

CompositionContainer

  • The GetExportsCore method can be overwritten. This is used to retrieve all exports of this CompositionContainer.
    • MEF also uses this to resolve dependencies.
  • In this context, Export instances are returned.
  • In GetExportsCore, the existing Export instances are packed into the wrapper, which is then able to provide information about the instances.

This derivation of CompositionContainer provides access to all retrieved, shared instances:

public class CustomCompositionContainer : CompositionContainer
{
    private readonly ConcurrentDictionary<object, object?> _retrievedSharedExports = new(ObjectReferenceEqualityComparer<object>.Default);

    public CustomCompositionContainer(ComposablePartCatalog? catalog, CompositionOptions compositionOptions, params ExportProvider[]? providers)
        : base(catalog, compositionOptions, providers)
    {
    }

    public IEnumerable<object> RetrievedSharedExports => this._retrievedSharedExports.Keys;

    protected override IEnumerable<Export>? GetExportsCore(ImportDefinition definition, AtomicComposition? atomicComposition)
    {
        var exports = base.GetExportsCore(definition, atomicComposition);
        if (exports is null)
        {
            yield break;
        }

        foreach (var item in exports)
        {
            if ((CreationPolicy?)item.Metadata.GetValueOrDefault("System.ComponentModel.Composition.CreationPolicy") == CreationPolicy.Shared)
            {
                yield return new VerboseExport(item, OnExportedValueRetrieved);
                continue;
            }

            yield return item;
        }

        void OnExportedValueRetrieved(object exportedValue) => this._retrievedSharedExports.TryAdd(exportedValue, null);
    }

    private sealed class VerboseExport : Export
    {
        private readonly Export _target;

        private readonly Action<object> _onExportedValueRetrieved;

        public VerboseExport(Export target, Action<object> onExportedValueRetrieved)
        {
            target.ThrowIfNull();
            onExportedValueRetrieved.ThrowIfNull();

            this._target = target;
            this._onExportedValueRetrieved = onExportedValueRetrieved;
        }

        public override ExportDefinition Definition => this._target.Definition;

        protected override object? GetExportedValueCore()
        {
            var result = this._target.Value;
            if (result is not null)
            {
                this._onExportedValueRetrieved(result);
            }

            return result;
        }
    }
}

The solution created above can now be simplified to the following code:

foreach (var item in compositionContainer.RetrievedSharedExports.OfType<IAsyncDisposable>())
{
    await item.DisposeAsync().ConfigureAwait(false);
}

This might not be the best solution but it's much cleaner than the previously used one that uses reflection for MEF internal types.

Upvotes: 0

Related Questions