.NET Core 中如何在運行中加載 Controller ?

最近工作都用 .NET Core Mvc 做開發,工作中遇到了比較特殊的需求:能不能在不停止網站,不重啓,在網站運行中加載新的 Controller 呢?

基於這個需求,分析下來,那就是動態加載一個已經編譯好的 dll 文件到當前運行時中,不用想,肯定使用到反射,讀取類型,把 Controller 類型的類加載到 MVC 的 controller 集合中。 

嗯,中國特色思維,先百度谷歌看看有麼有現成的吧!結果百度來谷歌去,沒有找到相關內容,都怪 .NET Core 太新了,網絡上還沒有太多相關的東西,只找到一個提問 :

ASP.Net Core register Controller at runtime

https://stackoverflow.com/questions/46156649/asp-net-core-register-controller-at-runtime

注:裏面有我的回答哦 => cnxiaoby

基於這個提問,我們知道了,ApplicationPart 是用來管理運行時中加載的 dll 的,只要能把帶有Controller的dll 加載到ApplicationParts,刷新一下相關的 runtime 就能實現了吧。有了思路,就先看看 .NET Core MVC 源碼吧,從裏面找找看有沒有相關的 Controller 緩存的集合,看能不能動態加載進去。 

由於工作忙,斷斷續續看了幾天源碼,過程波折就不細說了,最終找到了:

https://github.com/aspnet/Mvc/blob/rel/2.0.0/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs

比較關鍵的幾個地方:

        public ActionDescriptorCollectionProvider(
            IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
            IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
        {
            _actionDescriptorProviders = actionDescriptorProviders
                .OrderBy(p => p.Order)
                .ToArray();

            _actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();

            ChangeToken.OnChange(
                GetCompositeChangeToken,
                UpdateCollection);
        }       
        private IChangeToken GetCompositeChangeToken()
        {
            if (_actionDescriptorChangeProviders.Length == 1)
            {
                return _actionDescriptorChangeProviders[0].GetChangeToken();
            }

            var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
            for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
            {
                changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
            }

            return new CompositeChangeToken(changeTokens);
        }
        public ActionDescriptorCollection ActionDescriptors
        {
            get
            {
                if (_collection == null)
                {
                    UpdateCollection();
                }

                return _collection;
            }
        }

首先是屬性 ActionDescriptors ,它在 Controller/Action 的匹配中會用到,

其次是方法調用:ChangeToken.OnChange(GetCompositeChangeToken,  UpdateCollection); 一開始我也不懂ChangeToken,大家自行搜索,按照代碼字面意思,這裏註冊了一個 變更的 ,觸發條件是 GetCompositeChangeToken 這個方法的返回值,觸發變更操作的方法是  UpdateColection ,正好是更新 ActionDescriptors 集合,原來 .NET Core 早就幫我們做好了功能了!!再次證明 .NET Core 2.0 的強大。

 

找到了關鍵點,那我們就開始實驗吧:

具體代碼如下:

第一步:實現 IActionDescriptorChangeProvider 接口類:

public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
    public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();

    public CancellationTokenSource TokenSource { get; private set; }

    public bool HasChanged { get; set; }

    public IChangeToken GetChangeToken()
    {
        TokenSource = new CancellationTokenSource();
        return new CancellationChangeToken(TokenSource.Token);
    }
}

第二步:把實現類,在 Startup 中註冊到 Services 中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
    services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
}

第三步:在運行過程中加載 dll ,加載編譯好的 Controller 

public class TestController : Controller
{
    private readonly ApplicationPartManager _partManager;
    private readonly IHostingEnvironment _hostingEnvironment;
    public TestController(
        ApplicationPartManager partManager,
        IHostingEnvironment env)
    {
        _partManager = partManager;
        _hostingEnvironment = env;
    }
    public IActionResult RegisterControllerAtRuntime()
    {
        string assemblyPath = _hostingEnvironment.ContentRootPath + @"\DLL\Test.dll";
        var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
        if (assembly != null)
        {
            _partManager.ApplicationParts.Add(new AssemblyPart(assembly));
            // Notify change
            MyActionDescriptorChangeProvider.Instance.HasChanged = true;
            MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
            return Content("1");
        }
        return Content("0");
    }
}

Test.dll 中的源碼:

    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return Content("Test Home Index");
        }
    }

 

運行

瀏覽器訪問 http://localhost:81/Home/Index,返回 404,

瀏覽器訪問:http://localhost:81/Test/RegisterControllerAtRuntime ,返回 1,說明執行成功,

再訪問 http://localhost:81/Home/Index,返回 Test Home Index ,說明大功告成。

 

拓展:

基於這個需求,我們可以實現網站動態更新的功能了,或者其他腦洞更大的功能。

 

總結:

多讀讀源碼,瞭解它的原理,會對我們編程有很大的幫助,而且還能學到很多設計模式、範式,學到好的編碼規範、命名習慣。讓我們讀源碼像讀書一樣平常!

 

後續發現:

https://github.com/aspnet/Mvc/blob/rel/2.0.0/test/WebSites/ApiExplorerWebSite/Startup.cs

https://github.com/aspnet/Mvc/blob/rel/2.0.0/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerReloadableController.cs

這裏已經有相關類似用法示例了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章