.NET assembly resolving with complexe architecture - c#

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

Related

Save Process Object Information for use after its exit

On my C# Widows form, I want to make use of the Process object information even after its exit, but I am getting the exception “System process has exited, so the requested information is not available”.
What I tried thus far is to save it as a var and tag it with my ListView item, but it still throws the same exception.
//ListView Parameters (omitted redundant ones)
Process currentProcess = Process.GetProcessById(Convert.ToInt32(processStringID);
newListViewItem.Tag = currentProcess;
listView.Items.Add(newListViewItem);
I have an event of selected index changed, so when the user clicks on the ListView Item, it should show information about the process that was tagged with the item even if it has already exited.
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
try
{
Process processItem = (Process)listView.SelectedItems[0].Tag;
//Sample of getting process information (Error happens here)
MessageBox.Show(processItem.Name + processItem.VersionInfo);
}
catch (Exception ex)
{
throw ex;
}
}
Tldr; I need a way to save the entire Process object so I can get it's information later even if the process has exited. I am open to ideas as to how this can be implemented. Please assist me, as I am unable to think of any solution with my current understanding on programming.
Store it in a Session,
To store data:
Session["process"] = processItem;
To pull data from session:
var process = (Process)Session["process"]; // Don't forget to cast back to it's original type
Data is available even if you navigate to other pages unless you manually remove it.
Update:
Since question is not clear at first.
Create a global variable using static classes
public static class GlobalVar
{
/// <summary>
/// Global variable that is constant.
/// </summary>
public const string GlobalString = "Important Text";
/// <summary>
/// Static value protected by access routine.
/// </summary>
static int _globalValue;
/// <summary>
/// Access routine for global variable.
/// </summary>
public static int GlobalValue
{
get
{
return _globalValue;
}
set
{
_globalValue = value;
}
}
/// <summary>
/// Global static field.
/// </summary>
public static bool GlobalBoolean;
}
See this post : http://www.dotnetperls.com/global-variable

MemoryCache with regions support?

I need to add cache functionality and found a new shiny class called MemoryCache. However, I find MemoryCache a little bit crippled as it is (I'm in need of regions functionality). Among other things I need to add something like ClearAll(region). Authors made a great effort to keep this class without regions support, code like:
if (regionName != null)
{
throw new NotSupportedException(R.RegionName_not_supported);
}
flies in almost every method.
I don't see an easy way to override this behaviour. The only way to add region support that I can think of is to add a new class as a wrapper of MemoryCache rather then as a class that inherits from MemoryCache. Then in this new class create a Dictionary and let each method "buffer" region calls. Sounds nasty and wrong, but eventually...
Do you know of better ways to add regions to MemoryCache?
I know it is a long time since you asked this question, so this is not really an answer to you, but rather an addition for future readers.
I was also surprised to find that the standard implementation of MemoryCache does NOT support regions. It would have been so easy to provide right away. I therefore decided to wrap the MemoryCache in my own simple class to provide the functionality I often need.
I enclose my code it here to save time for others having the same need!
/// <summary>
/// =================================================================================================================
/// This is a static encapsulation of the Framework provided MemoryCache to make it easier to use.
/// - Keys can be of any type, not just strings.
/// - A typed Get method is provided for the common case where type of retrieved item actually is known.
/// - Exists method is provided.
/// - Except for the Set method with custom policy, some specific Set methods are also provided for convenience.
/// - One SetAbsolute method with remove callback is provided as an example.
/// The Set method can also be used for custom remove/update monitoring.
/// - Domain (or "region") functionality missing in default MemoryCache is provided.
/// This is very useful when adding items with identical keys but belonging to different domains.
/// Example: "Customer" with Id=1, and "Product" with Id=1
/// =================================================================================================================
/// </summary>
public static class MyCache
{
private const string KeySeparator = "_";
private const string DefaultDomain = "DefaultDomain";
private static MemoryCache Cache
{
get { return MemoryCache.Default; }
}
// -----------------------------------------------------------------------------------------------------------------------------
// The default instance of the MemoryCache is used.
// Memory usage can be configured in standard config file.
// -----------------------------------------------------------------------------------------------------------------------------
// cacheMemoryLimitMegabytes: The amount of maximum memory size to be used. Specified in megabytes.
// The default is zero, which indicates that the MemoryCache instance manages its own memory
// based on the amount of memory that is installed on the computer.
// physicalMemoryPercentage: The percentage of physical memory that the cache can use. It is specified as an integer value from 1 to 100.
// The default is zero, which indicates that the MemoryCache instance manages its own memory
// based on the amount of memory that is installed on the computer.
// pollingInterval: The time interval after which the cache implementation compares the current memory load with the
// absolute and percentage-based memory limits that are set for the cache instance.
// The default is two minutes.
// -----------------------------------------------------------------------------------------------------------------------------
// <configuration>
// <system.runtime.caching>
// <memoryCache>
// <namedCaches>
// <add name="default" cacheMemoryLimitMegabytes="0" physicalMemoryPercentage="0" pollingInterval="00:02:00" />
// </namedCaches>
// </memoryCache>
// </system.runtime.caching>
// </configuration>
// -----------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// Store an object and let it stay in cache until manually removed.
/// </summary>
public static void SetPermanent(string key, object data, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from write.
/// </summary>
public static void SetAbsolute(string key, object data, double minutes, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes) };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from write.
/// callback is a method to be triggered when item is removed
/// </summary>
public static void SetAbsolute(string key, object data, double minutes, CacheEntryRemovedCallback callback, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(minutes), RemovedCallback = callback };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an object and let it stay in cache x minutes from last write or read.
/// </summary>
public static void SetSliding(object key, object data, double minutes, string domain = null)
{
CacheItemPolicy policy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(minutes) };
Set(key, data, policy, domain);
}
/// <summary>
/// Store an item and let it stay in cache according to specified policy.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="data">Object to store</param>
/// <param name="policy">CacheItemPolicy</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static void Set(object key, object data, CacheItemPolicy policy, string domain = null)
{
Cache.Add(CombinedKey(key, domain), data, policy);
}
/// <summary>
/// Get typed item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static T Get<T>(object key, string domain = null)
{
return (T)Get(key, domain);
}
/// <summary>
/// Get item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static object Get(object key, string domain = null)
{
return Cache.Get(CombinedKey(key, domain));
}
/// <summary>
/// Check if item exists in cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static bool Exists(object key, string domain = null)
{
return Cache[CombinedKey(key, domain)] != null;
}
/// <summary>
/// Remove item from cache.
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
public static void Remove(object key, string domain = null)
{
Cache.Remove(CombinedKey(key, domain));
}
#region Support Methods
/// <summary>
/// Parse domain from combinedKey.
/// This method is exposed publicly because it can be useful in callback methods.
/// The key property of the callback argument will in our case be the combinedKey.
/// To be interpreted, it needs to be split into domain and key with these parse methods.
/// </summary>
public static string ParseDomain(string combinedKey)
{
return combinedKey.Substring(0, combinedKey.IndexOf(KeySeparator));
}
/// <summary>
/// Parse key from combinedKey.
/// This method is exposed publicly because it can be useful in callback methods.
/// The key property of the callback argument will in our case be the combinedKey.
/// To be interpreted, it needs to be split into domain and key with these parse methods.
/// </summary>
public static string ParseKey(string combinedKey)
{
return combinedKey.Substring(combinedKey.IndexOf(KeySeparator) + KeySeparator.Length);
}
/// <summary>
/// Create a combined key from given values.
/// The combined key is used when storing and retrieving from the inner MemoryCache instance.
/// Example: Product_76
/// </summary>
/// <param name="key">Key within specified domain</param>
/// <param name="domain">NULL will fallback to default domain</param>
private static string CombinedKey(object key, string domain)
{
return string.Format("{0}{1}{2}", string.IsNullOrEmpty(domain) ? DefaultDomain : domain, KeySeparator, key);
}
#endregion
}
You can create more than one just one MemoryCache instance, one for each partition of your data.
http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache.aspx :
you can create multiple instances of the MemoryCache class for use in the same application and in the same AppDomain instance
I just recently came across this problem. I know this is an old question but maybe this might be useful for some folks. Here is my iteration of the solution by Thomas F. Abraham
namespace CLRTest
{
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.Caching;
class Program
{
static void Main(string[] args)
{
CacheTester.TestCache();
}
}
public class SignaledChangeEventArgs : EventArgs
{
public string Name { get; private set; }
public SignaledChangeEventArgs(string name = null) { this.Name = name; }
}
/// <summary>
/// Cache change monitor that allows an app to fire a change notification
/// to all associated cache items.
/// </summary>
public class SignaledChangeMonitor : ChangeMonitor
{
// Shared across all SignaledChangeMonitors in the AppDomain
private static ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>> ListenerLookup =
new ConcurrentDictionary<string, EventHandler<SignaledChangeEventArgs>>();
private string _name;
private string _key;
private string _uniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
public override string UniqueId
{
get { return _uniqueId; }
}
public SignaledChangeMonitor(string key, string name)
{
_key = key;
_name = name;
// Register instance with the shared event
ListenerLookup[_uniqueId] = OnSignalRaised;
base.InitializationComplete();
}
public static void Signal(string name = null)
{
// Raise shared event to notify all subscribers
foreach (var subscriber in ListenerLookup.ToList())
{
subscriber.Value?.Invoke(null, new SignaledChangeEventArgs(name));
}
}
protected override void Dispose(bool disposing)
{
// Set delegate to null so it can't be accidentally called in Signal() while being disposed
ListenerLookup[_uniqueId] = null;
EventHandler<SignaledChangeEventArgs> outValue = null;
ListenerLookup.TryRemove(_uniqueId, out outValue);
}
private void OnSignalRaised(object sender, SignaledChangeEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.Name) || string.Compare(e.Name, _name, true) == 0)
{
// Cache objects are obligated to remove entry upon change notification.
base.OnChanged(null);
}
}
}
public static class CacheTester
{
private static Stopwatch _timer = new Stopwatch();
public static void TestCache()
{
MemoryCache cache = MemoryCache.Default;
int size = (int)1e6;
Start();
for (int idx = 0; idx < size; idx++)
{
cache.Add(idx.ToString(), "Value" + idx.ToString(), GetPolicy(idx, cache));
}
long prevCnt = cache.GetCount();
Stop($"Added {prevCnt} items");
Start();
SignaledChangeMonitor.Signal("NamedData");
Stop($"Removed {prevCnt - cache.GetCount()} entries");
prevCnt = cache.GetCount();
Start();
SignaledChangeMonitor.Signal();
Stop($"Removed {prevCnt - cache.GetCount()} entries");
}
private static CacheItemPolicy GetPolicy(int idx, MemoryCache cache)
{
string name = (idx % 10 == 0) ? "NamedData" : null;
CacheItemPolicy cip = new CacheItemPolicy();
cip.AbsoluteExpiration = System.DateTimeOffset.UtcNow.AddHours(1);
var monitor = new SignaledChangeMonitor(idx.ToString(), name);
cip.ChangeMonitors.Add(monitor);
return cip;
}
private static void Start()
{
_timer.Start();
}
private static void Stop(string msg = null)
{
_timer.Stop();
Console.WriteLine($"{msg} | {_timer.Elapsed.TotalSeconds} sec");
_timer.Reset();
}
}
}
His solution involved using an event to keep track of ChangeMonitors. But the dispose method was working slow when the number of entries were more than 10k. My guess is that this code SignaledChangeMonitor.Signaled -= OnSignalRaised removes a delegate from invocation list by doing a linear search. So when you remove a lot of entries it becomes slow. I decided to use ConcurrentDictionary instead of an event. In hope that dispose becomes faster. I ran some basic performance tests and here are the results:
Added 10000 items | 0.027697 sec
Removed 1000 entries | 0.0040669 sec
Removed 9000 entries | 0.0105687 sec
Added 100000 items | 0.5065736 sec
Removed 10000 entries | 0.0338991 sec
Removed 90000 entries | 0.1418357 sec
Added 1000000 items | 6.5994546 sec
Removed 100000 entries | 0.4176233 sec
Removed 900000 entries | 1.2514225 sec
I am not sure if my code does not have some critical flaws. I would like to know if that is the case.
Another approach is to implement a wrapper around MemoryCache that implements regions by composing the key and region name e.g.
public interface ICache
{
...
object Get(string key, string regionName = null);
...
}
public class MyCache : ICache
{
private readonly MemoryCache cache
public MyCache(MemoryCache cache)
{
this.cache = cache.
}
...
public object Get(string key, string regionName = null)
{
var regionKey = RegionKey(key, regionName);
return cache.Get(regionKey);
}
private string RegionKey(string key, string regionName)
{
// NB Implements region as a suffix, for prefix, swap order in the format
return string.IsNullOrEmpty(regionName) ? key : string.Format("{0}{1}{2}", key, "::", regionName);
}
...
}
It's not perfect but it works for most use cases.
I've implemented this and it's available as a NuGet package: Meerkat.Caching

How to manage classes from a loaded DLL?

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);
}
}

Is there something incorrect with this code? I don't believe I should have to create a new instance of object to modify collection

I am experiencing some weird behavior that disappears/reappears based on whether this dictionary is a new instance of the object, or the old instance of the object. Let me provide all the code first.
/// <summary>
/// Removes a control from monitoring/Session/Database based on ID.
/// </summary>
public static void Remove<T>(ICormantControl<T> control)
{
_logger.InfoFormat("Removing {0}", control.ID);
SerializableDictionary<string, T> states = new SerializableDictionary<string,T>(GetStates<SerializableDictionary<string, T>>());
((IDictionary)states).Remove(control.ID);
SetStates(states);
}
/// <summary>
/// Retrieves information on an object. If the object is cached to Session then the
/// cached object is retrieved. Else, it is retrieved from the database.
/// </summary>
/// <typeparam name="T"> The type of object expected to get back.</typeparam>
/// <returns> Collection of data for the specific object type.</returns>
public static T GetStates<T>() where T : new()
{
T states = new T();
string stateName = GetStateNameFromType(typeof(T));
if (!Equals(SessionRepository.Instance.GetSession(stateName), null))
{
states = (T)SessionRepository.Instance.GetSession(stateName);
}
else
{
XmlSerializer serializer = new XmlSerializer(states.GetType());
string data = DatabaseRepository.Instance.GetWebLayoutData(stateName);
if (!string.IsNullOrEmpty(data))
{
byte[] dataAsArray = Convert.FromBase64String(data);
MemoryStream stream = new MemoryStream(dataAsArray);
states = (T)serializer.Deserialize(stream);
}
SessionRepository.Instance.SetSession(stateName, states);
}
return states;
}
public static void SetStates<T>(T states) where T : new()
{
string stateName = GetStateNameFromType(typeof(T));
SessionRepository.Instance.SetSession(stateName, states);
if (shouldWriteToDatabase) DatabaseRepository.Instance.SaveToDatabase(stateName, states);
}
/// <summary>
/// Recreates the page state recursively by creating a control and looking for its known children.
/// </summary>
/// <param name="pane"> Pane having children added to it.</param>
private void RegeneratePaneChildren(CormantRadPane pane)
{
_logger.InfoFormat("Initializing paneToResize children for paneToResize {0}", pane.ID);
foreach (var splitterState in StateManager.GetStates<SerializableDictionary<string, RadSplitterSetting>>())
{
RadSplitterSetting splitterSetting = splitterState.Value;
if (!splitterSetting.ParentID.Contains(pane.ID)) continue;
CormantRadSplitter splitter = new CormantRadSplitter(splitterSetting);
pane.UpdatePanel.ContentTemplateContainer.Controls.AddAt(0, splitter); //Visibility will fight with splitter if you don't re-add like this.
RegenerateSplitterChildren(splitter);
}
}
/// <summary>
/// Recreates the page state recursively by creating a control and looking for its known children.
/// </summary>
/// <param name="splitter"> Splitter having children added to it. </param>
public void RegenerateSplitterChildren(RadSplitter splitter)
{
_logger.InfoFormat("Initializing splitter children for splitter {0}", splitter.ID);
foreach (var paneState in StateManager.GetStates<SerializableDictionary<string, RadPaneSetting>>()
.Where(paneState => paneState.Value.ParentID.Contains(splitter.ID)))
{
RadPaneSetting paneSetting = paneState.Value;
CormantRadPane pane = new CormantRadPane(paneSetting);
StyledUpdatePanel updatePanel = pane.CreateUpdatePanel(paneSetting.UpdatePanelID);
pane.Controls.Add(updatePanel);
splitter.Controls.Add(pane);
RegeneratePaneChildren(pane);
InsertSplitBar(splitter);
}
}
The key line to look at in all of this is: SerializableDictionary<string, T> states = new SerializableDictionary<string,T>(GetStates<SerializableDictionary<string, T>>());
If this line of code is modified such that it does not create a new instance of states (instead using the object saved in Session) my code gets 'desynched' and I experience odd behavior with my Regeneration methods. An object that is supposed to have 'ObjectA' as a parent instead has 'ObjectB' as a parent.
There's a lot of collection-modification going on... I'm removing a control from states and re-saving it...but I can't see where I do anything explicitly incorrect in this code. Yet, I still feel that I should be able to express the above line of code without creating a new instance of the object.
If anyone sees an obvious blunder I'd love to hear it. Thanks.

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.

Categories