How to manage classes from a loaded DLL? - c#

How can I get a class into a variable to call function and get properties?
I have been told to look into reflection but I don't get how it's done when the DLL was not known at compile time.
To be clear: I have a 'main' class which loads an DLL and I want to create an instance of a class within the DLL and directly get properties.

You should do something like this:
Assembly asm = Assembly.Load("DLL File Path"); //load DLL from file
Type t = asm.GetType("Test.ExternalDllTest"); //fully qualified name
dynamic oDynamic = Activator.CreateInstance(t, args);//create an instance of specified type, and assign it to DYNAMIC object
EDIT
oDynamic.SomeMethod(); //call the method of your type. Being DYNAMIC it will route the call to correct method.
Naturally, the DLL has to be a managed DLL (so written in .NET language)
I didn't compile this, honestly, but basically this is an idea of how to do that.
Here also an example, that may help.

In case you are talking about another .NET dll you can use this to load the assembly and get all the types in there:
var asm = Assembly.LoadFile("yourassembly.dll");
foreach (var t in asm.GetTypes())
{
Console.WriteLine("Type: {0}", t.Name);
}
You can instantiate an object with either the Activator:
Activator.CreateInstance(t, additional arguments);
or you can get a list of all public constructors for that type with GetConstructors:
var constructors = t.GetConstructors();
However unless you know what type you are looking for and what its constructor parameters are it is a bit pointless to try to instatiate and use it.

Example. Here is a method I use that searches a plug-in directory tree and returns a list of WPF ValueConverter classes...
private static List<IValueConverter> GetValueConverters( string rootDirectoryName)
{
List<IValueConverter> result = new List<IValueConverter>();
string[] exts = new string[]{"*.exe", "*.dll"};
DirectoryInfo di = new DirectoryInfo(rootDirectoryName);
foreach(string ext in exts)
{
foreach(FileInfo fi in (di.GetFiles(ext, SearchOption.AllDirectories)))
{
Assembly a = Assembly.LoadFrom(fi.FullName);
try
{
List<Type> ts = a.GetExportedTypes().ToList();
foreach (Type t in ts)
{
var d2 = t.GetInterfaces().Where(q => q.Name == "IValueConverter");
if (d2.Count() > 0)
{
result.Add(Activator.CreateInstance(t) as IValueConverter);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
return result;
}
The code searches through the directory tree for files matching 'dll' and 'exe'. When it finds one, it attempts to load it and then looks to see if there's any WPF ValueConverters in it.
If it finds one, the code creates an instances and adds it to the list, and ultimately returns the list. Of course the 'dll's' and 'exe's' must be in the managed world. And if you were interested in classes other than ValueConverters, you would have to change it accordingly.
This is a purpose built method (i.e., I know what's going to happen with the result) and the code is given here only as an example...

Write your self an Interface (IKnowAboutSomething) that is accessible via your Loaded DLL and also your Loading Program.
Scan through your Loaded DLL and find the classes which implement this interface.
Then you can use Activator.CreateInstance to create an instance of the Types you found (where you know the interface)
Now you just call methods on your IKnowAboutSomething.GetThingINeed() etc and you can interact with things you don't know about at compile time. (Well you know a little bit because they are using the Interface and therefore have an agreement)
Place this code in an External DLL (eg Core.Dll) that is accessible by both projects.
using System.IO;
public interface ISettings
{
/// <summary>
/// Will be called after you Setup has executed if it returns True for a save to be performed. You are given a Stream to write your data to.
/// </summary>
/// <param name="s"></param>
/// <remarks></remarks>
void Save(Stream s);
/// <summary>
/// Will be called before your Setup Method is to enable the loading of existing settings. If there is no previous configuration this method will NOT be called
/// </summary>
/// <param name="s"></param>
/// <remarks></remarks>
void Load(Stream s);
/// <summary>
/// Your plugin must setup a GUID that is unique for your project. The Main Program will check this and if it is duplicated your DLL will not load
/// </summary>
/// <value></value>
/// <returns></returns>
/// <remarks></remarks>
Guid Identifier { get; }
/// <summary>
/// This Description will be displayed for the user to select your Plugin. You should make this descriptive so the correct one is selected in the event they have multiple plugins active.
/// </summary>
/// <value></value>
/// <returns></returns>
/// <remarks></remarks>
string Description { get; }
}
Now in your Main Project add a Reference to the above DLL.
In your main project scan a directory for the DLL's you are planning to load (c:\myDlls*.dll) use a DirectoryInfo (or similar) to scan.
Once you have found a DLL, use the Assembly asm = Assembly.LoadFrom(filename) to get it loaded.
Now you can do this in the main project.
foreach (Type t in asm.GetTypes())
{
if (typeof(ISettings).IsAssignableFrom(t))
{
//Found a Class that is usable
ISettings loadedSetting = Activator.CreateInstance(t);
Console.WriteLine(loadedSetting.Description);
}
}

Related

Parse string to enum with custom attribute

I have an enum as follows. I also have a string in-progress I am trying to parse this string to the appropriate enum. As seen by the following test we want to parse to take a string and return the enum
[Fact]
public void TestParseOfEnum()
{
var data = "not-started";
var parsed = EnumExtensions.GetValueFromEnumMember<CarePlan.CarePlanActivityStatus>(data);
parsed.ShouldBe(CarePlan.CarePlanActivityStatus.NotStarted);
}
The issue being that enum try parse is checking on the name which means its failing. I need to parse it on this custom attribute.
CarePlan.CarePlanActivityStatus status
Enum.TryParse("in-progress", out status)
A try parse will not find this as try parse checks on the name field and not on the custom attribute within this enum. So this would return false and not find my enum
I have been trying to see how I could get a list back of all of the fields within the enum and then test on the literal.
This would work but i have to specify each of the values within the enum in getfield
var res = typeof(CarePlan.CarePlanActivityStatus)
.GetField(nameof(CarePlan.CarePlanActivityStatus.Cancelled))
.GetCustomAttribute<EnumLiteralAttribute>(false)
.Literal;
I tried something like this but Literal doesn't exist at this point apparently so this fails as well.
var hold = typeof(CarePlan.CarePlanActivityStatus).GetFields().Where(a =>
a.GetCustomAttributes<EnumLiteralAttribute>(false).Literal.Equals(data));
The enum
The nuget package i am using for the fhir library is Hl7.Fhir.R4
[FhirEnumeration("CarePlanActivityStatus")]
public enum CarePlanActivityStatus
{
/// <summary>
/// Care plan activity is planned but no action has yet been taken.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("not-started", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Not Started")] NotStarted,
/// <summary>
/// Appointment or other booking has occurred but activity has not yet begun.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("scheduled", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Scheduled")] Scheduled,
/// <summary>
/// Care plan activity has been started but is not yet complete.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("in-progress", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("In Progress")] InProgress,
/// <summary>
/// Care plan activity was started but has temporarily ceased with an expectation of resumption at a future time.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("on-hold", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("On Hold")] OnHold,
/// <summary>
/// Care plan activity has been completed (more or less) as planned.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("completed", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Completed")] Completed,
/// <summary>
/// The planned care plan activity has been withdrawn.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("cancelled", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Cancelled")] Cancelled,
/// <summary>
/// The planned care plan activity has been ended prior to completion after the activity was started.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("stopped", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Stopped")] Stopped,
/// <summary>
/// The current state of the care plan activity is not known. Note: This concept is not to be used for "other" - one of the listed statuses is presumed to apply, but the authoring/source system does not know which one.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("unknown", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Unknown")] Unknown,
/// <summary>
/// Care plan activity was entered in error and voided.
/// (system: http://hl7.org/fhir/care-plan-activity-status)
/// </summary>
[EnumLiteral("entered-in-error", "http://hl7.org/fhir/care-plan-activity-status"), Hl7.Fhir.Utility.Description("Entered in Error")] EnteredInError,
}
// example of everything I have tested with
The nuget package i am using for the fhir library is Hl7.Fhir.R4
public static class EnumExtensions
{
public static T GetValueFromEnumMember<T>(string value) where T : Enum
{
{
// Doesnt work as .Literal shows as red not valid.
var hold = typeof(CarePlan.CarePlanActivityStatus).GetFields().Where(a =>
a.GetCustomAttributes<EnumLiteralAttribute>(false).Literal.Equals(data));
// doesnt work as its returning the string i need it to return the enum
var res = typeof(CarePlan.CarePlanActivityStatus)
.GetField(nameof(CarePlan.CarePlanActivityStatus.Cancelled))
.GetCustomAttribute<EnumLiteralAttribute>(false)
.Literal;
// neither of the following two work as they are just looping though and i cant find the enum they find.
foreach (CarePlan.CarePlanActivityStatus status in (CarePlan.CarePlanActivityStatus[])Enum.GetValues(
typeof(CarePlan.CarePlanActivityStatus)))
{
}
foreach (string i in Enum.GetNames(typeof(CarePlan.CarePlanActivityStatus)))
{
// var res = typeof(CarePlan.CarePlanActivityStatus)
// .GetField(nameof(CarePlan.CarePlanActivityStatus[i]))
// .GetCustomAttribute<EnumLiteralAttribute>(false)
// .Literal;
//
// Console.WriteLine($" {res}" );
//
// Console.WriteLine($" {i}" );
}
}
Isnt there a way to parse a string to an enum without making a large mess of a if statements to test it. Ideally i need to create a generic method as i have about 10 of these enums i need test.
You can try with a extension method to read the Custom Atribute from Enums:
public static class EnumExtensions
{
public static T GetValueFromEnumMember<T>(string value) where T: Enum
{
var type = typeof(T);
foreach (var field in type.GetFields())
{
var attribute = Attribute.GetCustomAttribute(field,
typeof(EnumMemberAttribute)) as EnumMemberAttribute;
if (attribute != null)
{
if (attribute.Value == value)
return (T)field.GetValue(null);
}
else
{
if (field.Name == value)
return (T)field.GetValue(null);
}
}
throw new ArgumentException($"unknow value: {value}");
}
}
and use it like this:
EnumExtensions.GetValueFromEnumMember<CarePlanActivityStatus>(stringValueToTest)
it returns the Enum Value
I tried something like this but Literal doesn't exist at this point apparently so this fails as well.
One error in your question is that you're calling GetCustomAttributes rather than GetCustomAttribute. GetCustomAttributes returns an IEnumerable<EnumLiteralAttribute>, which won't of course have a Literal property. GetCustomAttribute returns a single EnumLiteralAttribute (or null), and you can acces its Literal property.
This would produce the error message:
error CS1061: 'IEnumerable<EnumLiteralAttribute>' does not contain a definition for 'Literal' and no accessible extension method 'Literal' accepting a first argument of type 'IEnumerable<EnumLiteralAttribute>' could be found (are you missing a using directive or an assembly reference?)
Another issue is that this will then fail with a NullReferenceException. If you look at what GetFields() is actually returning, the first field it returns is value__, which doesn't have any attributes on it. So your GetCustomAttribute<EnumLiteralAttribute>() returns null, and accessing its Literal member fails with a NullReferenceException.
If you exclude this field from your test, or just make sure that you're safe to GetCustomAttribute returning null, everything works fine.
E.g.:
var hold = typeof(CarePlanActivityStatus).GetFields()
.Where(a => a.GetCustomAttribute<EnumLiteralAttribute>(false)?.Literal == data);
See it on SharpLab.
If you then want to access the enum which has that attribute, call the FieldInfo's GetValue method:
var hold = typeof(CarePlanActivityStatus).GetFields()
.FirstOrDefault(a => a.GetCustomAttribute<EnumLiteralAttribute>(false)?.Literal == data)
?.GetValue(null);
See it on SharpLab.
From the comments, you can have multiple attributes on a single field, and you want to see if any of their Literal attributes matches data.
In this case you will want to use GetCustomAttributes to get all attributes, but you then want to loop through them and see if the Literal attribute on any of them matches what you want. The easiest way to do this is with Linq's Any:
string data = "cancelled";
var hold = typeof(CarePlanActivityStatus).GetFields()
.FirstOrDefault(a => a.GetCustomAttributes<EnumLiteralAttribute>(false)
.Any(x => x.Literal == data))
?.GetValue(null);
hold.Dump();
See it on SharpLab.
you can use this:
var enumType = typeof(CarePlanActivityStatus);
FieldInfo field = enumType.GetField(nameof(CarePlanActivityStatus.Cancelled));
var enumLiteralAttribute = field.GetCustomAttribute<EnumLiteralAttribute>(false);
WriteLine(enumLiteralAttribute.Name); // canceled
WriteLine(enumLiteralAttribute.Url); // http://hl7.org/fhir/care-plan-activity-status

Using Generics in MEF-Context

I want to use generics in combination with MEF-"[ImportMany(....))]", but I get compile-errors.
The following code without using generics works fine: The factory-class "HelperOneFactory" (see below) is searching für all implementations of the "IHelperOne"-interface. From this list it takes the first one without Metadata-value "Original". If it doesn't exist, it takes the first without checking the Metadata-value.
/// =====================================================================================
/// factory-implementation for interface-type "IHelperOne" (==> works fine)
/// =====================================================================================
[Export(typeof(HelperOneFactory))]
public class HelperOneFactory: IPartImportsSatisfiedNotification
{
[ImportMany(typeof(IHelperOne))]
private IEnumerable<Lazy<IHelperOne, Dictionary<string, object>>> Helper;
/// <summary>
/// reference to the relevant implementaion (HelperOneOriginal or HelperOneCusto)
/// </summary>
public IHelperOne Current { get; private set; }
/// <summary>
/// looking for all implementations of IHelperOne to find out the one to use
/// </summary>
public void OnImportsSatisfied()
{
Current = Helper.Count() > 1 ? Helper.First<Lazy<IHelperOne, Dictionary<string, object>>>(s => !s.Metadata.ContainsValue("Original")).Value :
Helper.First<Lazy<IHelperOne, Dictionary<string, object>>>().Value;
}
That works fine, but I have to implement factory-classes for many interface-types. So I try to use generics for the interface-type, but then I get compile-errors (using .NET Framework 4.6.1):
/// =====================================================================================
/// a version with using generic ==> compiler-errors !!
/// =====================================================================================
[Export(typeof(HelperTemplateFactory<>))]
public class HelperTemplateFactory<THelperInterface> : IPartImportsSatisfiedNotification
{
[ImportMany(typeof(THelperInterface))] // ==> ERROR: "Attribute argument cannot use type parameters"
[ImportMany(THelperInterface)] // ==> also ERROR: "Type parameter name is not valid at this point"
private IEnumerable<Lazy<THelperInterface, Dictionary<string, object>>> Helper;
...
Is it possible to use a generic type for the "ImportMany" command?
The context of the problem:
A "normal" class "HelperOneOriginal" is the Standard-version of the HelperOne and can be overwritten in Custom-Projects by defining a sub-class "HelperOneCustom", normaly placed in a separate VisualStudio-Project.
Both classes have the interface "IHelperOne".
The main-Program should use the Custom-class if defined, or otherwise the Original-class. The Original-class has the Metadata-Information "Orginal".
Therefore the "HelperOneFactory" looks for all realisations of "IHelperOne"-Interfaces and takes the first one without Metadata "Original".
If it doesn't exist it takes the Original-class. The reference to the relevant class ist stored inside the HelperClass in the member "Current" for using by the main-program.
If necessary a small test-project can be provided.
I suggest, I have to write an "answer" to mark the question als "resolved" =>
a line with only "[ImportMany]" is the solution!
Thanks to Dave M

.NET assembly resolving with complexe architecture

hope someone can answer this question.
I have an application with this architecture:
ApiLibrary (class library)
UsedLibrary version 2 (class library referenced by ApiLibrary)
Then, My ApiLibrary has a third parties plugins system.
Developpers can create custom plugins referencing ApiLibrary and extending an abstract type named "AbstractPlugin".
There is a specific folder (Plugins) in which users can put sub-folders, themselves containing dlls generated for plugins.
My API has a dedicated method to load these plugins, looping on all dlls files in this folders and using "Assembly.LoadFile(currentDll)".
Then, it loops on all types from the assembly and tries to find types that extends from AbstractPlugin. All these types found are a plugin that can be used within the API.
Plugins should not include the output of ApiLibrary in folders where they are placed (requirement specified to developpers). To ensure my API is effectively resolved when calling plugins functions, I handled the AppDomain.CurrentDomain.AssemblyResolve event and returns the executing assembly.
But they can include in their folders dlls of other libraries.
The problem is that now, I have a plugin that actually needs to reference "UsedLibrary", but in version 1.
Then, if within my ApiLibrary a function from the UsedLibrary is called before plugins are loaded, the version 2 is loaded and the plugin won't work because it needs the version 1.
Moreover, if plugins are loaded before, the version 1 is loaded and my API can't use functions from the v2.
In fact, I simplified the problem because it's really more complicated as UsedLibrary dynamically loads itself unmanaged libraries placed on the main folder of my API and the plugin should load unmanaged libraries from its own folder.
I'd like to know if anyone has a solution to ensure that my plugin will be able to call functions from the v1 and my API will call functions from the v2 (I can't rename these assemblies).
Thank you very much.
EDIT 1:
I tried to load DLLs in different application domains for each plugin folder but after many tries, could not finally get my assemblies. How can I load my assemblies in different appdomains using this kind of code:
loadedAssemblies = new Dictionary<string, Assembly>();
UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
string basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));
foreach (string fullPluginPath in Directory.EnumerateDirectories(PLUGINS_PATH))
{
string pluginFolder = Path.GetFileName(fullPluginPath);
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = pluginFolder;
setup.ApplicationBase = basePath;
setup.PrivateBinPath = fullPluginPath;
System.Security.PermissionSet permissionSet = new System.Security.PermissionSet(System.Security.Permissions.PermissionState.Unrestricted);
AppDomain pluginAppDomain = AppDomain.CreateDomain(pluginFolder, null, setup, permissionSet);
foreach (string fileName in Directory.EnumerateFiles(fullPluginPath))
{
if (Path.GetExtension(fileName.ToLower()) == ".dll")
{
try
{
Assembly currentAssembly = ??; // How to load the assembly within the plugin app domain ???
loadedAssemblies.Add(currentAssembly.FullName, currentAssembly);
}
catch (Exception e)
{
// DLL could not be loaded
}
}
}
}
Thank you very much.
Edit 2:
I finally understood how to comunicate between AppDomains and could load assemblies and find plugins in them, but I still have a problem.
Plugins are loaded by my API (a class library) though a PluginsManager object:
/// <summary>
/// A plugin manager can be used by the holding application using the API to gain access to plugins installed by the user.
/// All errors detected during plugins loading are stored, so applications may know when a plugin could not be loaded.
/// </summary>
public class PluginsManager : MarshalByRefObject
{
/// <summary>
/// Name of the plugins folder.
/// </summary>
private const string PLUGIN_FOLDER = "ApiPlugins";
#region Fields
/// <summary>
/// Plugins loaded and initialised without errors
/// </summary>
private List<AbstractPlugin> loadedPlugins;
/// <summary>
/// Dictionary of errors detected during DLL parsings.
/// </summary>
private Dictionary<string, Exception> dllLoadException;
/// <summary>
/// Dictionary of errors detected during assemblies types parsing.
/// </summary>
private Dictionary<string, Exception> assembliesTypesLoadExceptions;
/// <summary>
/// Dictionary of errors detected during plugins instance creation.
/// </summary>
private Dictionary<string, Exception> pluginsConstructionExceptions;
/// <summary>
/// Dictionary of errors detected during plugins instance creation.
/// </summary>
private Dictionary<string, Exception> pluginsRetrievalExceptions;
/// <summary>
/// Dictionary of errors detected during plugins initialisation.
/// </summary>
private Dictionary<string, Exception> pluginsInitialisationExceptions;
/// <summary>
/// The currently loaded DLL during plugins reload.
/// Used to resolve assembly in the current domain when loading an assembly containing IDM-CIC plugins.
/// </summary>
private static string currentlyLoadedDll = null;
#endregion
#region Methods
public void LoadPlugins()
{
// Ensures assemblies containing plugins will be loaded in the current domain
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
try
{
List<PluginLoader> pluginLoaders = new List<PluginLoader>();
loadedAssemblies = new Dictionary<string, Assembly>();
loadedPlugins = new List<AbstractPlugin>();
dllLoadException = new Dictionary<string, Exception>();
assembliesTypesLoadExceptions = new Dictionary<string, Exception>();
pluginsInitialisationExceptions = new Dictionary<string, Exception>();
pluginsConstructionExceptions = new Dictionary<string, Exception>();
pluginsRetrievalExceptions = new Dictionary<string, Exception>();
string pluginsFolderPath = Path.Combine("C:", PLUGIN_FOLDER);
UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
string basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));
// detect automatically dll files in plugins folder and load them.
if (Directory.Exists(pluginsFolderPath))
{
foreach (string pluginPath in Directory.EnumerateDirectories(pluginsFolderPath))
{
string pluginFolderName = Path.GetFileName(pluginPath);
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = pluginFolderName;
setup.ApplicationBase = basePath;
setup.PrivateBinPath = pluginPath;
PermissionSet permissionSet = new PermissionSet(PermissionState.Unrestricted);
AppDomain pluginAppDomain = AppDomain.CreateDomain(pluginFolderName, AppDomain.CurrentDomain.Evidence, setup, permissionSet);
foreach (string dllFile in Directory.EnumerateFiles(pluginPath, "*.dll", SearchOption.TopDirectoryOnly))
{
try
{
currentlyLoadedDll = dllFile;
PluginLoader plugLoader = (PluginLoader)pluginAppDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(PluginLoader).FullName);
Assembly ass = plugLoader.LoadAssemblyIfItContainsPlugin(dllFile);
if (ass != null)
{
pluginLoaders.Add(plugLoader);
}
// Check types parsing exceptions and store them
if (plugLoader.CaughtExceptionOnTypesParsing != null)
{
assembliesTypesLoadExceptions.Add(plugLoader.LoadedAssemblyName, plugLoader.CaughtExceptionOnTypesParsing);
}
}
catch (Exception e)
{
// Store problem while loading a DLL
dllLoadException.Add(dllFile, e);
}
}
}
}
foreach (PluginLoader plugLoader in pluginLoaders)
{
// Load all plugins of the loaded assembly
plugLoader.LoadAllPlugins();
// Check plugins construction errors and store them
foreach (KeyValuePair<Type, Exception> kvp in plugLoader.CaughtExceptionOnPluginsCreation)
{
Type type = kvp.Key;
Exception e = kvp.Value;
pluginsConstructionExceptions.Add(type.Name + " from " + plugLoader.LoadedAssemblyName, e);
}
for (int i = 0; i < plugLoader.GetPluginsCount(); i++)
{
AbstractPlugin plugin = null;
try
{
// Try to retrieve the plugin in our context (should be OK because AbstractPlugin extends MarshalByRefObject)
plugin = plugLoader.GetPlugin(i);
}
catch (Exception e)
{
// Store the retrieval error
pluginsRetrievalExceptions.Add(plugLoader.GetPluginName(i) + " from " + plugLoader.LoadedAssemblyName, e);
}
if (plugin != null)
{
try
{
// Initialise the plugin through the exposed method in AbstractPlugin type and that can be overridden in plugins
plugin.Initialise();
loadedPlugins.Add(plugin);
}
catch (Exception e)
{
// Store the initialisation error
pluginsInitialisationExceptions.Add(plugin.GetType().Name, e);
}
}
}
}
}
finally
{
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
}
}
/// <summary>
/// Ensure plugins assemblies are loaded also in the current domain
/// </summary>
/// <param name="sender">Sender of the event</param>
/// <param name="args">Arguments for assembly resolving</param>
/// <returns>The resolved assembly or null if not found (will result in a dependency error)</returns>
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
AssemblyName assemblyName = new AssemblyName(args.Name);
if (args.RequestingAssembly == null && assemblyName.Name.ToLower() == Path.GetFileNameWithoutExtension(currentlyLoadedDll).ToLower())
{
return Assembly.LoadFrom(currentlyLoadedDll);
}
return null;
}
/// <summary>
/// Enumerates all plugins loaded and initialised without error.
/// </summary>
/// <returns>Enumeration of AbstractPlugin</returns>
public IEnumerable<AbstractPlugin> GetPlugins()
{
return loadedPlugins;
}
#endregion
}
The PluginLoader object that help to retrieve plugins is described as it:
/// <summary>
/// This class is internally used by the PluginsManager to load an assembly though a DLL file and load an instance of each plugins that can be found within this assembly.
/// </summary>
internal class PluginLoader : MarshalByRefObject
{
#region Fields
/// <summary>
/// The assembly loaded within this plugin loader.
/// Null if could not be loaded or does not contains any plugin.
/// </summary>
private Assembly loadedAssembly = null;
/// <summary>
/// Exception caught when trying to parse assembly types.
/// Null if GetTypes() was successfull.
/// </summary>
private Exception caughtExceptionOnTypesParsing = null;
/// <summary>
/// Dictionary of exceptions caught when trying ti instantiate plugins.
/// The key is the plugin type and the value is the exception.
/// </summary>
private Dictionary<Type, Exception> caughtExceptionOnPluginsCreation = new Dictionary<Type, Exception>();
/// <summary>
/// The list of loaded plugins that is filled when calling the LoadAllPlugins method.
/// </summary>
private List<AbstractPlugin> loadedPlugins = new List<AbstractPlugin>();
#endregion
#region Accessors
/// <summary>
/// Gets the loaded assembly name if so.
/// </summary>
public string LoadedAssemblyName
{
get { return loadedAssembly != null ? loadedAssembly.FullName : null; }
}
/// <summary>
/// Gets the exception caught when trying to parse assembly types.
/// Null if GetTypes() was successfull.
/// </summary>
public Exception CaughtExceptionOnTypesParsing
{
get { return caughtExceptionOnTypesParsing; }
}
/// <summary>
/// Gets an enumeration of exceptions caught when trying ti instantiate plugins.
/// The key is the plugin type and the value is the exception.
/// </summary>
public IEnumerable<KeyValuePair<Type, Exception>> CaughtExceptionOnPluginsCreation
{
get { return caughtExceptionOnPluginsCreation; }
}
#endregion
#region Methods
/// <summary>
/// Loads an assembly through a DLL path and returns it only if it contains at least one plugin.
/// </summary>
/// <param name="assemblyPath">The path to the assembly file</param>
/// <returns>An assembly or null</returns>
public Assembly LoadAssemblyIfItContainsPlugin(string assemblyPath)
{
// Load the assembly
Assembly assembly = Assembly.LoadFrom(assemblyPath);
IEnumerable<Type> types = null;
try
{
types = assembly.GetTypes();
}
catch (Exception e)
{
// Could not retrieve types. Store the exception
caughtExceptionOnTypesParsing = e;
}
if (types != null)
{
foreach (Type t in types)
{
if (!t.IsAbstract && t.IsSubclassOf(typeof(AbstractPlugin)))
{
// There is a plugin. Store the loaded assembly and return it.
loadedAssembly = assembly;
return loadedAssembly;
}
}
}
// No assembly to return
return null;
}
/// <summary>
/// Load all plugins that can be found within the assembly.
/// </summary>
public void LoadAllPlugins()
{
if (caughtExceptionOnTypesParsing == null)
{
foreach (Type t in loadedAssembly.GetTypes())
{
if (!t.IsAbstract && t.IsSubclassOf(typeof(AbstractPlugin)))
{
AbstractPlugin plugin = null;
try
{
plugin = (AbstractPlugin)Activator.CreateInstance(t);
}
catch (Exception e)
{
caughtExceptionOnPluginsCreation.Add(t, e);
}
if (plugin != null)
{
loadedPlugins.Add(plugin);
}
}
}
}
}
/// <summary>
/// Returns the number of loaded plugins.
/// </summary>
/// <returns>The number of loaded plugins</returns>
public int GetPluginsCount()
{
return loadedPlugins.Count;
}
/// <summary>
/// Returns a plugin name from its index in the list of loaded plugins.
/// </summary>
/// <param name="index">The index to search</param>
/// <returns>The name of the corresponding plugin</returns>
public string GetPluginName(int index)
{
return loadedPlugins[index].Name;
}
/// <summary>
/// Returns a plugin given its index in the list of loaded plugins.
/// </summary>
/// <param name="index">The index to search</param>
/// <returns>The loaded plugin as AbstractPlugin</returns>
public AbstractPlugin GetPlugin(int index)
{
return loadedPlugins[index];
}
#endregion
}
I noticed that to ensure the communication, all objects that can be passed between the current domain and plugins domains must extends the MarshalByRefObject clas (or has the Serializable attribute but I want to communicate between AppDomains and not copying objects in current domain). Its OK for this, I ensured that all needed objects extend MarshalByRefObject type.
This API is referenced by the user application (another visual studio project) that calls a PluginsManager to load all plugins and iterate on loaded plugins.
It can successfully load and iterate all plugins, but in the AbstractPlugin type, I have specific events that must be handled by the application. When creating a handle on these events, I have a SerializationException...
Here is the plugins controller in my application:
/// <summary>
/// This controller helps to load all plugins and display their controls within the application
/// </summary>
internal class PluginsController
{
/// <summary>
/// A plugins manager that helps to retrieve plugins
/// </summary>
private PluginsManager pluginsManager = new PluginsManager();
/// <summary>
/// Initialise the list of available plugins and create all needed controls to the application
/// </summary>
internal void InitialisePlugins()
{
pluginsManager.LoadPlugins();
foreach (AbstractPlugin plugin in pluginsManager.GetPlugins())
{
// Treat my plugin data, adding controls to the application
// ...
// Handle events on the plugin : EXCEPTION
plugin.OnControlAdded += plugin_OnControlAdded;
plugin.OnControlChanged += plugin_OnControlChanged;
plugin.OnControlRemoved += plugin_OnControlRemoved;
}
}
void plugin_OnControlAdded(AbstractPlugin plugin, PluginControl addedControl)
{
// Handle control added by the plugin and add the new control to the application
}
void plugin_OnControlChanged(AbstractPlugin plugin, PluginControl changedControl)
{
// Handle control changed by the plugin and updates the concerned control in the application
}
void plugin_OnControlRemoved(AbstractPlugin plugin, PluginControl removedControl)
{
// Handle control removed by the plugin and remove the control from the application
}
}
So, the PluginsController class must also extend the MarshalByRefObject class because methods added to events must be sent through the proxy.
I have many other problems because of the complexe architecture of my API and some functions can't be used.
If I load my plugins in the current domain, everything works but there may be problems between assemblies version.
I'm thinking that I won't be able to perform what I want...
So I will only ensure that my API has all its dependencies loaded before plugins are loaded and plugins may not work if an assembly is not compatible with the previous version that the plugin uses.
If anyone has any other suggestion...
Thank you.
You could put each plugin in it's own app domain and load your dlls into that app domain. See here for an example
Other solutions could be:
Aplly strong naming to the assembly and install the UsedLibrary in both versions to the GAC, but this needs to be done for every installation and I don't think you have controll over the libraries loaded
If the dll-versions are compatible, you could use an assembly-redirect to always use the newer one of both dlls

How to load user control given its type

Is there a way to instantiate and use a usercontrol (.ascx) if given you have its Type (ie. typeof(MyUserControl))?
With regular asp.net controls like a textbox or a dropdownlist you can just make a new instance and add it to a controls collection. This doesnt appear to work for User controls. While you can make a new instance and add it to a collection, and have all of its events fire, it will not actually render to the page. Typically you would call Page.LoadControl() with the path to the .ascx
This presents a problem if all you have is its type. How can you get the path to the .ascx to give to the LoadControl method. Ideally I would also like to not have to have a reference to the Page object
No. This is not possible; the ASCX virtual path must be supplied to dynamically load a User Control with markup and there is no internal mapping of types of virtual paths.
However, because I am still lazy, here is the approach I used that is still "type safe", and isolates the resolving issue to a single location in code. It still requires access to the "Page object", but otherwise takes care of the silly details.
Here is the brief explanation:
Use the type to look-up the relative (to my root) ASCX path using a heuristic to map types from namespace to virtual path; allow a way to specify a manual mapping, and use that if specified
Turn the relative path of the type into the correct/full virtual path
Load the control with the virtual path
Continue as though nothing has happened
Enjoy (I just copy'n'pasted select parts from my project, YMMV):
/// <summary>
/// Load the control with the given type.
/// </summary>
public object LoadControl(Type t, Page page)
{
try
{
// The definition for the resolver is in the next code bit
var partialPath = resolver.ResolvePartialPath(t);
var fullPath = ResolvePartialControlPath(partialPath);
// Now we have the Control loaded from the Virtual Path. Easy.
return page.LoadControl(fullPath);
} catch (Exception ex)
{
throw new Exception("Control mapping failed", ex);
}
}
/// <summary>
/// Loads a control by a particular type.
/// (Strong-typed wrapper for the previous method).
/// </summary>
public T LoadControl<T>(Page page) where T : Control
{
try
{
return (T)LoadControl(typeof (T), page);
} catch (Exception ex)
{
throw new Exception(string.Format(
"Failed to load control for type: {0}",
typeof (T).Name), ex);
}
}
/// <summary>
/// Given a partial control path, return the full (relative to root) control path.
/// </summary>
string ResolvePartialControlPath(string partialPath)
{
return string.Format("{0}{1}.ascx",
ControlPathRoot, partialPath);
}
The code listing for ControlResolver:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FooBar
{
class ControlResolver
{
const string BaseNamespace = "TheBaseControlNameSpace";
readonly IDictionary<Type, string> resolvedPaths = new Dictionary<Type, string>();
/// <summary>
/// Maps types to partial paths for controls.
///
/// This is required for Types which are NOT automatically resolveable
/// by the simple reflection mapping.
/// </summary>
static readonly IDictionary<Type, string> MappedPartialPaths = new Dictionary<Type, string>
{
{ typeof(MyOddType), "Relative/ControlPath" }, // No virtual ~BASE, no .ASXC
};
/// <summary>
/// Given a type, return the partial path to the ASCX.
///
/// This path is the path UNDER the Control Template root
/// WITHOUT the ASCX extension.
///
/// This is required because there is no mapping maintained between the class
/// and the code-behind path.
///
/// Does not return null.
/// </summary>
public string ResolvePartialPath (Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
string partialPath;
if (resolvedPaths.TryGetValue(type, out partialPath))
{
return partialPath;
} else
{
string mappedPath;
if (MappedPartialPaths.TryGetValue(type, out mappedPath))
{
resolvedPaths[type] = mappedPath;
return mappedPath;
} else
{
// Since I use well-mapped virtual directory names to namespaces,
// this gets around needing to manually specify all the types above.
if (!type.FullName.StartsWith(BaseNamespace))
{
throw new InvalidOperationException("Invalid control type");
} else
{
var reflectionPath = type.FullName
.Replace(BaseNamespace, "")
.Replace('.', '/');
resolvedPaths[type] = reflectionPath;
return reflectionPath;
}
}
}
}
}
}
' Load the control
Dim myUC as UserControl = LoadControl("ucHeading.ascx")
' Set the Usercontrol Type
Dim ucType as Type = myUC.GetType()
' Get access to the property
Dim ucPageHeadingProperty as PropertyInfo = ucType.GetProperty("PageHeading")
' Set the property
ucPageHeadingProperty.SetValue(myUC,"Access a Usercontrol from Code Behind",Nothing)
pnlHeading.Controls.Add ( myUC )
You may be out of luck here.
This fellow was not able to get help doing this.
And they told this person it can't be done.

Obsolete attribute causes property to be ignored by XmlSerialization

I'm refactoring some objects that are serialized to XML but need to keep a few properties for backwards compatibility, I've got a method that converts the old object into the new one for me and nulls the obsolete property. I want to use the Obsolete attribute to tell other developers not to use this property but it is causing the property to be ignored by the XmlSerializer.
Similar Code:
[Serializable]
public class MySerializableObject
{
private MyObject _oldObject;
private MyObject _anotherOldObject;
private MyObject _newBetterObject;
[Obsolete("Use new properties in NewBetterObject to prevent duplication")]
public MyObject OldObject
{
get { return _oldObject; }
set { _oldObject = value; }
}
[Obsolete("Use new properties in NewBetterObject to prevent duplication")]
public MyObject AnotherOldObject
{
get { return _anotherOldObject; }
set { _anotherOldObject = value; }
}
public MyObject NewBetterObject
{
get { return _anotherOldObject; }
set { _anotherOldObject = value; }
}
}
Any ideas on a workaround? My best solution is to write obsolete in the XML comments...
Update: I'm using .NET 2.0
EDIT: After reading a MS Connect article, it appears that .Net 2.0 has a 'feature' where it makes ObsoleteAttribute equivalent to XmlIgnoreAttribute without any notification in the documentation. So I'm going to revise my answer to say that the only way to have your cake and eat it too in this instance is to follow #Will's advice and implement serialization manually. This will be your only future proof way of including Obsolete properties in your XML. It is not pretty in .Net 2.0, but .Net 3.0+ can make life easier.
From XmlSerializer:
Objects marked with the Obsolete Attribute no longer serialized
In the .NET Framework 3.5 the XmlSerializer class no longer serializes objects that are marked as [Obsolete].
Another workaround is to subscribe to XmlSerializer.UnknownElement, when creating the serializer for the datatype, and then fix old data that way.
http://weblogs.asp.net/psteele/archive/2011/01/31/xml-serialization-and-the-obsolete-attribute.aspx
Maybe consider to have the method for subscribing as a static method on the class for datatype.
static void serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
if( e.Element.Name != "Hobbies")
{
return;
}
var target = (MyData) e.ObjectBeingDeserialized;
foreach(XmlElement hobby in e.Element.ChildNodes)
{
target.Hobbies.Add(hobby.InnerText);
target.HobbyData.Add(new Hobby{Name = hobby.InnerText});
}
}
I have struggled with this a lot - there is no solution other than doing serialization manually or using another serializer.
However, instead of writing shims for each obsolete property which quickly becomes a pain, you could consider adding an Obsolete prefix to property names (e.g. Foo becomes ObsoleteFoo. This will not generate a compiler warning like the attribute will, but at least it's visible in code.
1) WAG: Try adding the XmlAttributeAttribute to the property; perhaps this will override the ObsoleteAttribute
2) PITA: Implement IXmlSerializable
Yes I agree with marking things with the name "Obsolete" we do this with Enum values
/// <summary>
/// Determines the swap file location for a cluster.
/// </summary>
/// <remarks>This enum contains the original text based values for backwards compatibility with versions previous to "8.1".</remarks>
public enum VMwareClusterSwapFileLocation
{
/// <summary>
/// The swap file location is unknown.
/// </summary>
Unknown = 0,
/// <summary>
/// The swap file is stored in the virtual machine directory.
/// </summary>
VmDirectory = 1,
/// <summary>
/// The swap file is stored in the datastore specified by the host.
/// </summary>
HostLocal = 2,
/// <summary>
/// The swap file is stored in the virtual machine directory. This value is obsolete and used for backwards compatibility.
/// </summary>
[XmlElement("vmDirectory")]
ObseleteVmDirectory = 3,
/// <summary>
/// The swap file is stored in the datastore specified by the host. This value is obsolete and used for backwards compatibility.
/// </summary>
[XmlElement("hostLocal")]
ObseleteHostLocal = 4,
}
You may try the following workaround:
add a method named
ShouldSerializeOldObject ()
{
return true;
}
ShouldSerializeAnotherOldObject ()
{
return true
}
this may override the obsolete Attribute

Categories