簡體   English   中英

如何在MEF中處理具有相同依賴項的不同版本的模塊?

[英]How can I deal with modules with different versions of the same dependencies in MEF?

目前,我已經配置了一個模塊文件夾,並且所有模塊程序集及其依賴項都存在於那里。 我擔心在六個月的時間內,有人會構建一個新模塊,並且它的依賴項會覆蓋舊版本的依賴項。

我是否應該開發某種模塊注冊表,開發人員注冊一個新模塊,並在modules文件夾中為其分配一個子文件夾名稱? 如果我必須告訴主機有關模塊的話,這種方法會削弱使用DirectoryCatalog的便利性。

我過去也遇到過類似的問題。 下面我介紹我的解決方案,我認為這與您要完成的工作類似。

像這樣使用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 )。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM