如何在MEF中处理具有相同依赖项的不同版本的模块?

时间:2014-01-28 07:17:38

标签: c# .net wpf mef composition

目前,我已经配置了一个模块文件夹,并且所有模块程序集及其依赖项都存在于那里。我担心在六个月的时间内,有人会构建一个新模块,并且它的依赖项会覆盖旧版本的依赖项。

我是否应该开发某种模块注册表,开发人员注册一个新模块,并在模块文件夹中为其分配一个子文件夹名称?如果我必须告诉主持人有关模块的话,这种方法会削弱使用DirectoryCatalog的便利性。

1 个答案:

答案 0 :(得分:4)

过去我遇到过类似的问题。下面我介绍我的解决方案,我认为这与您要完成的工作类似。

像这样使用MEF真的很吸引人,但这是我的谨慎之处:

  • 快速复杂
  • 你必须做出一些妥协,比如继承MarshalByRefObject和插件不能用解决方案构建
  • 而且,正如我所说,更简单更好!其他非MEF设计可能是更好的选择。

好的,免责声明......

.NET允许您将同一程序集的多个版本加载到内存中,但不能卸载它们。这就是为什么我的方法需要AppDomain才能在新版本可用时卸载模块的原因。

下面的解决方案允许您在运行时将插件dll复制到bin目录中的“plugins”文件夹中。随着新插件的添加和旧插件的覆盖,旧的插件将被卸载,新的插件将被加载,而无需重新启动您的应用程序。如果您的目录中同时有多个具有不同版本的dll,您可能需要修改PluginHost以通过文件的属性读取程序集版本并相应地执行操作。

有三个项目:

  • ConsoleApplication.dll(仅限参考Integration.dll)
  • Integration.dll
  • TestPlugin.dll(引用Integration.dll,必须复制到ConsoleApplication bin / Debug / plugins)

ConsoleApplication.dll

class Program
{
    static void Main(string[] args)
    {
        var pluginHost = new PluginHost();
        //Console.WriteLine("\r\nProgram:\r\n" + string.Join("\r\n", AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name)));
        pluginHost.CallEach<ITestPlugin>(testPlugin => testPlugin.DoSomething());
        //Console.ReadLine();
    }
}

Integration.dll

PluginHost允许您与插件通信。应该只有一个PluginHost实例。这也可以作为民意调查DirectoryCatalog

public class PluginHost
{
    public const string PluginRelativePath = @"plugins";
    private static readonly object SyncRoot = new object();
    private readonly string _pluginDirectory;
    private const string PluginDomainName = "Plugins";
    private readonly Dictionary<string, DateTime> _pluginModifiedDateDictionary = new Dictionary<string, DateTime>();
    private PluginDomain _domain;

    public PluginHost()
    {
        _pluginDirectory = AppDomain.CurrentDomain.BaseDirectory + PluginRelativePath;
        CreatePluginDomain(PluginDomainName, _pluginDirectory);
        Task.Factory.StartNew(() => CheckForPluginUpdatesForever(PluginDomainName, _pluginDirectory));
    }

    private void CreatePluginDomain(string pluginDomainName, string pluginDirectory)
    {
        _domain = new PluginDomain(pluginDomainName, pluginDirectory);
        var files = GetPluginFiles(pluginDirectory);
        _pluginModifiedDateDictionary.Clear();
        foreach (var file in files)
        {
            _pluginModifiedDateDictionary[file] = File.GetLastWriteTime(file);
        }
    }
    public void CallEach<T>(Action<T> call) where T : IPlugin
    {
        lock (SyncRoot)
        {
            var plugins = _domain.Resolve<IEnumerable<T>>();
            if (plugins == null)
                return;
            foreach (var plugin in plugins)
            {
                call(plugin);
            }
        }
    }

    private void CheckForPluginUpdatesForever(string pluginDomainName, string pluginDirectory)
    {
        TryCheckForPluginUpdates(pluginDomainName, pluginDirectory);
        Task.Delay(5000).ContinueWith(task => CheckForPluginUpdatesForever(pluginDomainName, pluginDirectory));
    }

    private void TryCheckForPluginUpdates(string pluginDomainName, string pluginDirectory)
    {
        try
        {
            CheckForPluginUpdates(pluginDomainName, pluginDirectory);
        }
        catch (Exception ex)
        {
            throw new Exception("Failed to check for plugin updates.", ex);
        }
    }

    private void CheckForPluginUpdates(string pluginDomainName, string pluginDirectory)
    {
        var arePluginsUpdated = ArePluginsUpdated(pluginDirectory);
        if (arePluginsUpdated)
            RecreatePluginDomain(pluginDomainName, pluginDirectory);
    }

    private bool ArePluginsUpdated(string pluginDirectory)
    {
        var files = GetPluginFiles(pluginDirectory);
        if (IsFileCountChanged(files))
            return true;
        return AreModifiedDatesChanged(files);
    }

    private static List<string> GetPluginFiles(string pluginDirectory)
    {
        if (!Directory.Exists(pluginDirectory))
            return new List<string>();
        return Directory.GetFiles(pluginDirectory, "*.dll").ToList();
    }

    private bool IsFileCountChanged(List<string> files)
    {
        return files.Count > _pluginModifiedDateDictionary.Count || files.Count < _pluginModifiedDateDictionary.Count;
    }

    private bool AreModifiedDatesChanged(List<string> files)
    {
        return files.Any(IsModifiedDateChanged);
    }

    private bool IsModifiedDateChanged(string file)
    {
        DateTime oldModifiedDate;
        if (!_pluginModifiedDateDictionary.TryGetValue(file, out oldModifiedDate))
            return true;
        var newModifiedDate = File.GetLastWriteTime(file);
        return oldModifiedDate != newModifiedDate;
    }

    private void RecreatePluginDomain(string pluginDomainName, string pluginDirectory)
    {
        lock (SyncRoot)
        {
            DestroyPluginDomain();
            CreatePluginDomain(pluginDomainName, pluginDirectory);
        }
    }

    private void DestroyPluginDomain()
    {
        if (_domain != null)
            _domain.Dispose();
    }
}

Autofac是此代码的必需依赖项。 PluginDomainDependencyResolver在插件AppDomain中实例化。

[Serializable]
internal class PluginDomainDependencyResolver : MarshalByRefObject
{
    private readonly IContainer _container;
    private readonly List<string> _typesThatFailedToResolve = new List<string>();

    public PluginDomainDependencyResolver()
    {
        _container = BuildContainer();
    }

    public T Resolve<T>() where T : class
    {
        var typeName = typeof(T).FullName;
        var resolveWillFail = _typesThatFailedToResolve.Contains(typeName);
        if (resolveWillFail)
            return null;
        var instance = ResolveIfExists<T>();
        if (instance != null)
            return instance;
        _typesThatFailedToResolve.Add(typeName);
        return null;
    }

    private T ResolveIfExists<T>() where T : class
    {
        T instance;
        _container.TryResolve(out instance);
        return instance;
    }

    private static IContainer BuildContainer()
    {
        var builder = new ContainerBuilder();

        var assemblies = LoadAssemblies();
        builder.RegisterAssemblyModules(assemblies); // Should we allow plugins to load dependencies in the Autofac container?
        builder.RegisterAssemblyTypes(assemblies)
            .Where(t => typeof(ITestPlugin).IsAssignableFrom(t))
            .As<ITestPlugin>()
            .SingleInstance();

        return builder.Build();
    }

    private static Assembly[] LoadAssemblies()
    {
        var path = AppDomain.CurrentDomain.BaseDirectory + PluginHost.PluginRelativePath;
        if (!Directory.Exists(path))
            return new Assembly[]{};
        var dlls = Directory.GetFiles(path, "*.dll").ToList();
        dlls = GetAllDllsThatAreNotAlreadyLoaded(dlls);
        var assemblies = dlls.Select(LoadAssembly).ToArray();
        return assemblies;
    }

    private static List<string> GetAllDllsThatAreNotAlreadyLoaded(List<string> dlls)
    {
        var alreadyLoadedDllNames = GetAppDomainLoadedAssemblyNames();
        return dlls.Where(dll => !IsAlreadyLoaded(alreadyLoadedDllNames, dll)).ToList();
    }

    private static List<string> GetAppDomainLoadedAssemblyNames()
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        return assemblies.Select(a => a.GetName().Name).ToList();
    }

    private static bool IsAlreadyLoaded(List<string> alreadyLoadedDllNames, string file)
    {
        var fileInfo = new FileInfo(file);
        var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty);
        return alreadyLoadedDllNames.Any(dll => dll == name);
    }

    private static Assembly LoadAssembly(string path)
    {
        return Assembly.Load(File.ReadAllBytes(path));
    }
}

此类表示实际的插件AppDomain。解析到此域的程序集应首先从bin / plugins文件夹加载所需的任何依赖项,然后加载bin文件夹,因为它是父AppDomain的一部分。

internal class PluginDomain : IDisposable
{
    private readonly string _name;
    private readonly string _pluginDllPath;
    private readonly AppDomain _domain;
    private readonly PluginDomainDependencyResolver _container;

    public PluginDomain(string name, string pluginDllPath)
    {
        _name = name;
        _pluginDllPath = pluginDllPath;
        _domain = CreateAppDomain();
        _container = CreateInstance<PluginDomainDependencyResolver>();
    }

    public AppDomain CreateAppDomain()
    {
        var domaininfo = new AppDomainSetup
        {
            PrivateBinPath = _pluginDllPath
        };
        var evidence = AppDomain.CurrentDomain.Evidence;
        return AppDomain.CreateDomain(_name, evidence, domaininfo);
    }

    private T CreateInstance<T>()
    {
        var assemblyName = typeof(T).Assembly.GetName().Name + ".dll";
        var typeName = typeof(T).FullName;
        if (typeName == null)
            throw new Exception(string.Format("Type {0} had a null name.", typeof(T).FullName));
        return (T)_domain.CreateInstanceFromAndUnwrap(assemblyName, typeName);
    }

    public T Resolve<T>() where T : class
    {
        return _container.Resolve<T>();
    }

    public void Dispose()
    {
        DestroyAppDomain();
    }

    private void DestroyAppDomain()
    {
        AppDomain.Unload(_domain);
    }
}

最后你的插件接口。

public interface IPlugin
{
    // Marker Interface
}

主应用程序需要了解每个插件,因此需要一个接口。他们必须继承IPlugin并在PluginHost BuildContainer方法

中注册
public interface ITestPlugin : IPlugin
{
    void DoSomething();
}

TestPlugin.dll

[Serializable]
public class TestPlugin : MarshalByRefObject, ITestPlugin
{
    public void DoSomething()
    {
        //Console.WriteLine("\r\nTestPlugin:\r\n" + string.Join("\r\n", AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name)));
    }
}

最后的想法......

此解决方案对我有用的一个原因是我的AppDomain插件实例的生命周期非常短。但是,我相信可以进行修改以支持具有更长生命周期的插件对象。这可能需要一些妥协,例如更高级的插件包装器,它可能在重新加载AppDomain时重新创建对象(参见CallEach)。