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

标签 c# .net wpf mef composition

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

我是否应该开发某种模块注册表,开发人员可以在其中注册一个新模块,并在模块文件夹中为其分配一个子文件夹名称?但是,如果我必须告诉主机有关模块的信息,这种情况会降低使用 DirectoryCatalog 的便利性。

最佳答案

我以前遇到过类似的问题。下面我介绍我的解决方案,我认为这与您要实现的目标相似。

像这样使用 MEF 真的很有趣,但这里是我的警告:

  • 它很快就变得复杂了
  • 您必须做出一些妥协,例如继承 MarshalByRefObject 和不使用解决方案构建的插件
  • 而且,正如我所决定的,越简单越好!其他非 MEF 设计可能是更好的选择。

好吧,免责声明……

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

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

一共有三个项目:

  • ConsoleApplication.dll(仅限 References Integration.dll)
  • 集成.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();
    }
}

集成.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);
    }
}

最后是您的插件接口(interface)。

public interface IPlugin
{
    // Marker Interface
}

主应用程序需要了解每个插件,因此需要一个接口(interface)。它们必须继承IPlugin并注册到PluginHostBuildContainer方法

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

测试插件.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)。

关于c# - 我如何处理 MEF 中具有相同依赖项的不同版本的模块?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21399196/

相关文章:

.net - 如何在Web api 2中启用跨域请求

wpf - 在 WPF 中测量文本

c# - 如何使用 C# DirectoryInfo 枚举所有子目录的文件?

c# - 为什么无法覆盖 getter-only 属性并添加 setter?

c# - 为什么C#只允许方法的最后一个参数是 "variable length"

c# - 需要 SharpDXElement 替代品。 sharpDX WPF 闪烁的解决方法

C# 获取焦点窗口?

c# - 如何在 C# 中使用 RANSAC 过滤 OpenSURF 无效匹配

c# - 为什么检查这个 != null?

c# - 解决编译器错误 CS1519 : Invalid token ',' in class, 结构或接口(interface)成员声明