This question seems to have been asked several times, but I have yet to find any answer that actually works. Very simply, how do I remove something from a MEF container.
Even the code shown here https://mef.codeplex.com/wikipage?title=Parts%20Lifetime under AddPart/RemovePart doesn't work as it won't compile as it is listed. The code shows this:
var catalog = new AssemblyCatalog(typeof(Program).Assembly);
var container = new CompositionContainer(catalog);
var root = new Root();
// add external part
container.ComposeParts(root);
// ... use the composed root instance
// removes external part
batch = new CompositionBatch();
batch.RemovePart(root);
container.Compose(batch);
This won't compile because the call to RemovePart requires a ComposablePart which root is clearly not. Other simple examples show how to create the part and remove the part, but a reference to the created part (as ComposablePart) is remembered so the removal just uses that reference. I don't want to keep a reference to each part in the container whenever they are created, I just want to remove a part from the container at any arbitrary point in my application without having to keep a reference to it throughout.
Here is what I am trying to do using the exact same pattern listed in the documentation linked above:
public class Program
{
[Import]
private IClass myClass;
public Program()
{
var container = new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
container.ComposeParts(this);
var partToRemove = container.GetExport<IClass>();
var batch = new CompositionBatch();
batch.RemovePart(partToRemove);
container.Compose(batch);
}
public static void Main(string[] args)
{
var program = new Program();
}
}
But this gives me the following compilation error:
Argument 1: cannot convert from
'System.Lazy' to
'System.ComponentModel.Composition.Primitives.ComposablePart' C:\Users\irbldr.CORP\Documents\Visual
Studio 2012\Projects\ConsoleApplication4\Program.cs
Which is exactly the same error I get if I use the code directly from the documentation linked above.
Is there no way to simply remove something from the MEF container?
This link could be helpful: How do I get MEF to recompose when I change a part?
But long story short: you can remove from MEF an instance, but you cannot remove an export definition without to drop the complete catalog, recompose and filter it (simply not add something).
Related
I'm implementing a framework for C# on ASP.NET using Dotnet 6. I want part of the framework to be extensible by outside parties; they just need to implement a few classes and we can integrate their work via Nuget or direct assembly reference.
Part of what they need to complete is a Registration Class so they can define how their engine should be registered with the dependency injection container. This is an example of what I'd expect 3rd parties to supply:
public class EchoServiceRegistration : IRegisterDI
{
public IServiceCollection Register(IServiceCollection serviceCollection)
{
serviceCollection.TryAddSingleton<EchoEngine>();
return serviceCollection;
}
}
In the consuming application, I'm looking for all of the classes that implement the IRegisterDI interface using the AppDomain class, which is a riff off this SO answer https://stackoverflow.com/a/26750/2573109. (Note: I switched to using name-based matching just for troubleshooting and will change it back to a better implementation once properly solved):
List<Type> allRegistrationClasses = AppDomain
.CurrentDomain
.GetAssemblies()
.SelectMany(it => it.GetTypes())
.Where(tp => tp.GetInterfaces()
.Any(inter => inter.Name == nameof(IRegisterDI)))
.ToList();
As written, this returns 0 types.
On the next troubleshooting iteration, I proved that the registration class is available to the caller. So, I manually created an instance of the EchoServiceRegistration class, as seen below. This time, the AppDomain contains a single entry for EchoServiceRegistration (as expected).
var allRegistrationClasses = AppDomain.CurrentDomain.GetAssemblies().SelectMany(it => it.GetTypes()).Where(tp => tp.GetInterfaces().Any(inter => inter.Name == nameof(IRegisterDI))).ToList();
var echoServiceRegistration = new EchoServiceRegistration();
echoServiceRegistration.Register(builder.Services);
if (allRegistrationClasses.Count is not 1) throw new InvalidOperationException(); // Does not throw an exception
To prove I didn't accidentally fix something, I commented out the two lines related to Echo Service Registration, and allRegistrationClasses again contains 0 records.
var allRegistrationClasses = AppDomain.CurrentDomain.GetAssemblies().SelectMany(it => it.GetTypes()).Where(tp => tp.GetInterfaces().Any(inter => inter.Name == nameof(IRegisterDI))).ToList();
// var echoServiceRegistration = new EchoServiceRegistration();
// echoServiceRegistration.Register(builder.Services);
if (allRegistrationClasses.Count is not 1) throw new InvalidOperationException(); // Throws an exception
My gut reaction is I don't understand how the AppDomain determines what assemblies to load. I started reading the documentation Microsoft provides about it, but it seems like a deep topic and I won't have a more clear understanding without quite a bit of dedicated reading.
How do I guarantee the full set of classes are available when calling this during the DI Container build? Please let me know if I can provide any additional detail or clarity.
It looks to me like on your first test, the assembly is not loaded into the app domain because it's not used.
On your second test, you are manually creating an instance so you are forcing the loading of the assembly. Note that even if the assembly is added as a reference, it won't be loaded unless it's actually needed.
You need to explicitly load the assemblies for them to be scanned. One way of doing this would be to have all extension libraries live in a particular folder from which you can then load them before registration:
var files = Directory.GetFiles(#"C:\my-framework\extensions");
foreach (var file in files)
{
var extension = Path.GetExtension(file);
if(extension == ".dll")
{
var asm = Assembly.LoadFile(file);
Console.WriteLine($"Loaded extension [{asm.FullName}]");
}
}
Once the assemblies are loaded, you should get the desired results.
This is an overly-simplified example but it should be enough to get you going.
I need to run code from a text file, in the context of the current form (code). One of the requirements is to have the code create and add a new control to the current form.
For example, in Form1.cs:
using System.Windows.Forms;
...
public int[] someCoords = { 20, 10 };
public string someImportantString = "Hello";
public void SayHello() {
MessageBox.Show("Hello world.");
}
private void runCodeInForm() {
// theCode will be read from a text file
string theCode = #"
// Has System.Windows.Forms already added in form
Button newButton = new Button();
newButton.Text = someImportantString; // Hello
newButton.Location = new Point(someCoords[0], someCoords[1]); // 20, 10
// Add this button to the current form
this.Controls.Add(newButton);
this.SayHello(); // Says hello. Just an example function.
";
// Execute theCode in the current form
CodeRunner.Execute(theCode, this);
}
I have tried using CSharpCodeProvider, but it seems like this can only compile the code as a separate program.
I would like this because I want the user to be able to change this code (text file) to what they would like. It is not specifically just for creating controls, but that functionality will be needed.
I am not worried about the security of the program.
Consider these points to solve the problem:
You should create your dynamic code as a class in a dll.
Your class should implement a specific interface or have a known method like Run. So you can call the method later when the code compiles.
Your class or the known method should accept some parameters to receive the context variables. These context variables can include a Form as parameter. You can also encapsulate context parameters in a class/interface or to keep it simple you can rely on dynamic for passing parameters.
Then to run the dynamic code, first compile it, then pass context parameters to the class or the known method and call the known method.
Example
Here is a quick and dirty example of how you can compile and run a code at run-time and let the code use your context:
public string SomePublicField = "Hello!";
private void button1_Click(object sender, EventArgs e) {
var csc = new CSharpCodeProvider();
var parameters = new CompilerParameters(new[] {
"mscorlib.dll",
"System.Windows.Forms.dll",
"System.dll",
"System.Drawing.dll",
"System.Core.dll",
"Microsoft.CSharp.dll"});
var results = csc.CompileAssemblyFromSource(parameters,
#"
using System.Windows.Forms;
using System.Drawing;
public class Sample
{
public void DoSomething (dynamic form)
{
var b = new Button();
b.Text = form.Text;
b.Click += (s,e)=>{MessageBox.Show(form.SomePublicField);};
form.Controls.Add(b);
}
}");
//Check if compilation is successful, run the code
if (!results.Errors.HasErrors) {
var t = results.CompiledAssembly.GetType("Sample");
dynamic o = Activator.CreateInstance(t);
o.DoSomething(this);
}
else {
var errors = string.Join(Environment.NewLine,
results.Errors.Cast<CompilerError>().Select(x => x.ErrorText));
MessageBox.Show(errors);
}
}
Basically what it seems you want to do, is dynamically compile and add code to your current program. This is not impossible and there are various ways to do it.
The most common uses for this type of functionality are plugins and scripting systems. However there are caveats with the both of these.
One of the of the biggest draw backs is that to run code that you have compiled (however it is you are doing that), you need to literally load it as an assembly into you app domain using the standard, Load methods. Once a library is loaded you actually cant unload it with out unloading the app domain, so this creates problems in certain situations .
If this is a scripting thing you are after, i would seriously consider using a pre-built scripting library (and there are many). Some use nifty-tricks to make this work well and have done a lot of the hard work for you... As an example http://csscript.net/
However be prepared! plugin and scripting systems start off easy, yet they are incredibly fiddly to make stable and workable. I would suggest exploring the domain of your problem first, and make sure you are not trying to reinvent the wheel... There are many options for serialisation and loading object parameters at run-time safely without fuss
Good luck
I am using IoC in my solution which is split into the following projects:
Toolbox.DAL
Toolbox.BL
Toolbox.EntityFramework
Toolbox.API (main project)
To register the components in my main project, I use the following bit of code:
using (WindsorContainer container = new WindsorContainer())
{
AssemblyFilter filter = new AssemblyFilter(HttpRuntime.BinDirectory, "Toolbox.*.dll");
IWindsorInstaller installer = FromAssembly.InDirectory(filter);
return new CastleInitialiser(container.Install(installer));
}
However, this is causing an issue where the dependencies in my main project are being registered twice - once during normal initialisation and then again from the compiled assembly.
Is there any way to change the filter so it will pick up all assemblies except the current project assembly? I have been searching around but could not find any decent examples of how to use the assembly filter properly.
Just looked at the source code
You can add additional filters.
using (WindsorContainer container = new WindsorContainer())
{
AssemblyFilter filter = new AssemblyFilter(HttpRuntime.BinDirectory, "Toolbox.*.dll");
//this
filter = filter.FilterByName(name => !name.Name.Equals("Toolbox.API"));
IWindsorInstaller installer = FromAssembly.InDirectory(filter);
return new CastleInitialiser(container.Install(installer));
}
My syntax might not be 100% correct, but it should illustrate how you can do it.
You can filter your assemblies by name:
AssemblyFilter filter =
new AssemblyFilter(HttpRuntime.BinDirectory, "Toolbox.*.dll")
.FilterByName(an => !an.Name.StartsWith("Toolbox.API");
I've got an ITagger and an IWpfTextViewMargin, both are exported as MEF components. I want to import the ITagger in my Margin code, and then use some members in that Tagger.
Now I tried to use ComponentContainer in the Margin class, then import the IViewTaggerProvider. I used the following code, which can be found in many MEF tutorials
[Import(typeof(IViewTaggerProvider))]
public IViewTaggerProvider vt_provider { get; set; }
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog(typeof(TestMargin).Assembly));
_container = new CompositionContainer(catalog);
//Fill the imports of this object
try
{
this._container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
System.Diagnostics.Trace.WriteLine(compositionException.Message);
}
and the export code.
[Export(typeof(IViewTaggerProvider))]
[ContentType...
The exported class is defined in another namespace but same assembly.
Here I got problem that ComposeParts(this) throws ImportCardinalityMismatchException. I don't know why the parameter is this. I tried to pass the catalog to it, there is no exception but the import is also null. I also referred to debug mef failures and believe that the exported class has the right contract name and export type identity.
After checking the assembly with Visual MEFx and debugging, I found that probably it's because the IViewTaggerProvider imports a Visual Studio IClassificationTypeRegistryService, which is also an MEF part and results in a rejection of the IViewTaggerProvider.
[Primary Rejection]
[Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No valid exports were found that match the constraint '((exportDefinition.ContractName == "Microsoft.VisualStudio.Text.Classification.IClassificationTypeRegistryService") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Microsoft.VisualStudio.Text.Classification.IClassificationTypeRegistryService".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.
So one solution is to add the assembly that exports IClassificationTypeRegistryService. It's a Visual Studio core editor service but I cannot find which assembly exports it. Anyone knows this?
Or any better possible solutions?
Try VisualMEFx. Here is a short blog entry about getting started https://ihadthisideaonce.com/2012/02/22/getting-started-with-visual-mefx/. Once you have it up and running, use VisualMEFx to load the TestMargin assembly and see if any IViewTaggerProvider is exported from that assembly.
Also remember that ImportCardinalityMistmatch doesn't only mean that an export is missing. It can also mean that there are too many exports available that can satisfy the import and MEF has no way of choosing which one to use. So when you are examining your composition in VisualMEFx, check to see if there are too many.
This parameter:
void Bootstrap()
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog(typeof(TestMargin).Assembly));
_container = new CompositionContainer(catalog);
//Fill the imports of this object
try
{
var objectToSatisfy = this;
// var objectToSatifsy = new SomeOtherObjectWithImports();
this._container.ComposeParts(objectToSatisfy);
}
catch (CompositionException compositionException)
{
System.Diagnostics.Trace.WriteLine(compositionException.Message);
}
}
When you call ComposeParts you pass an object to the method. MEF will take the object that you pass and see if there are any imports on it that need to be satisfied. If it finds any imports, it will look in the catalog and try to satisfy them. You can pass any object you want to the ComposeParts method. So I've modified your sample code a little to show two different options. One option is to create some object that needs to be satisfied, and then give it to the container for composition. This is what I have done in the commented out line var objectToSatisfy = new SomeOtherObjectWithImports(). But it is often the case that the object we want to compose is the same object that's calling ComposeParts. So we don't need to create a new object to pass to the container, we already have the object, we just need a reference to it. In C# we can get a reference to the current object instance using the keyword this (in VB.NET the keyword is Me). So, when we want to satisfy imports on the same object that is calling ComposeParts, we can do so by using the this reference as the argument to ComposeParts.
The argument to the ComposeParts method is a parameter array. Informally, this just means that when you write container.ComposeParts(this) it is interpreted as if you had written container.ComposeParts(new object[] { this }). In practice this means you can pass multiple objects to MEF at once, like this:
container.ComposeParts(this, objectToSatifsy, thirdObjectToCompose);
If the object calling ComposeParts has no imports on it, then you should not be using this as the argument. Instead, create an object of the type that you want to compose and pass that to the method. Also, unless all the parts that you want to compose are available in the TestMargin assembly, you need to create more AssemlbyCatalogs for the assemblies that do provide the parts and add them to your AggregateCatalog.
If I define a class in a file
~/App_Code/Extensions/MyExtension/MyClass.cs
Is it possible to retrieve the filename by type (or 'MyExtension' part of it) without hard coding it?
var extTypes = getExtensions();
foreach(var extType in extTypes)
{
// something like
var files = extType.GetSourceFiles();
//or maybe asp.net keeps track of types in the dynamically created assembly
var files2 = SomeAspNetClass.WhereDidThisTypeComeFrom(extType);
}
Or inject it to the class in any way?
[ThisFile]
public class MyClass : MyBase
{
private string _file = <thisfile>;
}
This sort of information is only really available in compiled languages (like C#) when you have debugging symbols included. So if you had a Debug build then you could get at this information by examining the current StackFrame.
var stackFrame = new StackFrame(true);
stackFrame.GetFileName()
Of course you probably don't want to have debug builds on your production code, so it may be worth-while looking at alternate ways to achieve whatever it is your trying to do here.
maybe you are looking for this?
Assembly.GetAssembly(typeof(YourTypeHere)).Location