dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.95k stars 4.65k forks source link

Proposal: Inject existing object into MEF2 #29400

Closed gthvidsten closed 2 years ago

gthvidsten commented 5 years ago

MEF1 has method ComposeExportedValue<T>(T exportedValue). MEF2 should have a similar functionality in System.Composition.Hosting.ConfigurationContainer (as discussed in dotnet/runtime#18624 and dotnet/runtime#15362)

Suggested API

public ContainerConfiguration WithInstance<TExport> (TExport instance);
public ContainerConfiguration WithInstance<TExport> (string contractName, TExport instance);
public ContainerConfiguration WithInstance (Type t, object instance);
public ContainerConfiguration WithInstance (Type t, string contractName, object instance);

(Thanks to @NEKIT-Boss for the examples in dotnet/runtime#18624)

Usage

ContainerConfiguration containerConfig = new ContainerConfiguration()
    .WithAssembly(GetType().Assembly)
    .WithInstance<IExample>(new Example());

var container = containerConfig.CreateContainer();
var example = container.GetExport<IExample>();
danmoseley commented 5 years ago

Unfortunately we have no real expertise in MEF remaining on the team. @weshaggard did you work on this code?

weshaggard commented 5 years ago

I'v reviewed some of the MEF2 stuff but I didn't code it. I did however code the MEF1 stuff many years ago and from what I know of MEF2 the APIs seems like they would be reasonable but I would suggest someone doing a proof of concept implementation of them before formally accepting the API proposal. The reason for that is to ensure that these fit into the model that MEF2 uses.

danmoseley commented 5 years ago

Thanks @weshaggard . Does anyone around have knowledge of MEF2 specifically?

weshaggard commented 5 years ago

I doubt there is anyone that has any recent knowledge but @nblumhardt wrote it originally and might have some memory of the code.

nblumhardt commented 5 years ago

Hey @danmosemsft @weshaggard @gthvidsten !

This is a pretty straightforward addition; I wrote and included it as an extension WithExport() in the original MEF2 Code drop, along with another useful one, WithFactoryDelegate():

var config = new ContainerConfiguration()
    .WithExport<TextWriter>(Console.Out)
    .WithFactoryDelegate<string>(() => DateTime.Now.ToString(), isShared: true)
    .WithPart<Program>();

The terminology WithExport() could be the some bikeshedding but as far as I remember it's the most accurate WRT the underlying model.

The demo project is here: https://github.com/MicrosoftArchive/mef/blob/master/oob/demo/Microsoft.Composition.Demos.ExtendedPartTypes/Program.cs#L24

The extension method WithExport() just adds an InstanceExportDescriptorProvider to the container:

namespace Microsoft.Composition.Demos.ExtendedPartTypes.Extension
{
    static class ContainerConfigurationExtensions
    {
        public static ContainerConfiguration WithExport<T>(this ContainerConfiguration configuration, T exportedInstance, string contractName = null, IDictionary<string, object> metadata = null)
        {
            return WithExport(configuration, exportedInstance, typeof(T), contractName, metadata);
        }

        public static ContainerConfiguration WithExport(this ContainerConfiguration configuration, object exportedInstance, Type contractType, string contractName = null, IDictionary<string, object> metadata = null)
        {
            return configuration.WithProvider(new InstanceExportDescriptorProvider(
                exportedInstance, contractType, contractName, metadata));
        }

And the InstanceExportDescriptorProvider itself is pretty straightforward:

    // we're just aiming to show the mechanics here.
    class InstanceExportDescriptorProvider : SinglePartExportDescriptorProvider
    {
        object _exportedInstance;

        public InstanceExportDescriptorProvider(object exportedInstance, Type contractType, string contractName, IDictionary<string, object> metadata)
            : base (contractType, contractName, metadata)
        {
            if (exportedInstance == null) throw new ArgumentNullException("exportedInstance");
            _exportedInstance = exportedInstance;
        }

        public override IEnumerable<ExportDescriptorPromise> GetExportDescriptors(CompositionContract contract, DependencyAccessor descriptorAccessor)
        {
            if (IsSupportedContract(contract))
                yield return new ExportDescriptorPromise(contract, _exportedInstance.ToString(), true, NoDependencies, _ =>
                    ExportDescriptor.Create((c, o) => _exportedInstance, Metadata));
        }
    }

Since it's all based on the public API/extension points, you can drop this in today @gthvidsten and it should "just work".

It'd be wonderful to see this added to the official package, or included in the docs somewhere, if anyone picks it up (it still pains me that there's so much great stuff buried in this little project).

bryanchen-d commented 5 years ago

+1 vote for this feature.

ericstj commented 4 years ago

Thanks for the input @nblumhardt, I've gone ahead and marked this one ready-for-review. Cheers!

terrajobst commented 4 years ago

I'd go with @nblumhardt's proposal, slightly adjusted:

namespace System.Composition.Hosting
{
    public class ContainerConfiguration
    {
        public ContainerConfiguration WithExport<TExport>(TExport exportedInstance);
        public ContainerConfiguration WithExport<TExport>(TExport exportedInstance, string? contractName = null, IDictionary<string, object>? metadata = null);

        public ContainerConfiguration WithExport(Type contractType, object exportedInstance);
        public ContainerConfiguration WithExport(Type contractType, object exportedInstance, string? contractName = null, IDictionary<string, object>? metadata = null);
    }
}  
bartonjs commented 4 years ago

Video

After some discussion on the name of the methods, the use of defaults parameters, and the name of the generic, it's approved as

namespace System.Composition.Hosting
{
    public class ContainerConfiguration
    {
        public ContainerConfiguration WithExport<TExport>(TExport exportedInstance);
        public ContainerConfiguration WithExport<TExport>(TExport exportedInstance, string? contractName = null, IDictionary<string, object>? metadata = null);

        public ContainerConfiguration WithExport(Type contractType, object exportedInstance);
        public ContainerConfiguration WithExport(Type contractType, object exportedInstance, string? contractName = null, IDictionary<string, object>? metadata = null);
    }
} 

We also think it would be valueable to add an analyzer that warns when generic inference was used. (e.g. WithExport(new SomeExportedType()))

ericstj commented 4 years ago

@gthvidsten @nblumhardt this API was approved, were either of you going to drive the implementation in? If we want this in .NET 5.0 it would need to be merged before 8/18 or sooner.

nblumhardt commented 4 years ago

I may get a chance - a bit short of time, though, so I'll be pleased if someone else beats me to it :-)

danmoseley commented 4 years ago

Setting milestone to indicate this is not a required part of the 5.0 product.

ZigMeowNyan commented 2 years ago

Should also close issue #27324.

As the person who originally opened #15362 back in 2015, I'm glad to see this finally make it into the official packages. It drastically simplifies migration from MEF1 to MEF2 for people who don't need the flexibility of MEF1 and want to avoid the potentially higher performance cost. Refactoring a project to use plugins is a lot easier when you don't have to fully rearchitect to fit design assumptions and can just export an existing instance.

I'll be glad to review my legacy projects and adjust for this. Might even have time to swap out the old handwritten context classes for AssemblyLoadContext.