聲明:本文借鑒蔣金楠先生的博客: https://www.cnblogs.com/lonelyxmas/p/12656993.html
如何動態 的注冊Controller,大概思路是 使用 Roslyn解析並編譯代碼生成dll,利用IActionDescriptorProvider 接口,將生成好的ControllerActionDescriptor添加到ActionDescriptorCollection 集合中
動態生成 assembly 接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace DynamicRegister1
{
public interface ICompiler
{
/// <summary>
/// 動態生成 Assembly,使用 Roslyn 來生成
/// </summary>
/// <param name="text">需要動態編譯的代碼</param>
/// <param name="assemblies"> 生成assembly索要依賴的程序集</param>
/// <returns></returns>
Assembly Compile(string text, string assemblyName = null, params Assembly[] assemblies);
/// <summary>
/// 動態生成 Assembly,並保存到文件夾,使用 Roslyn 來生成
/// </summary>
/// <param name="text">需要動態編譯的代碼</param>
/// <param name="assemblies">生成assembly索要依賴的程序集</param>
/// <returns></returns>
string DynamicGenerateDllToFile(string text, string assemblyName = null, params Assembly[] assemblies);
}
}
2 對上面接口簡單的實現
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace DynamicRegister1
{
public class CompilerService : ICompiler
{
public Assembly Compile(string text, string assemblyName = null, params Assembly[] assemblies)
{
//引入相關的動態編譯包
//Microsoft.CodeAnalysis.CSharp;
// 加載相關的引用文件
var references = assemblies.Select(i => MetadataReference.CreateFromFile(i.Location));
// 類型為 csharp 動態鏈接庫
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
// 將文本生成解析樹
var syntaxTrees = new SyntaxTree[] { CSharpSyntaxTree.ParseText(text) };
// 程序集名稱,如果未空,生成新的程序集名稱
if (assemblyName == null)
assemblyName = "_" + Guid.NewGuid().ToString("D");
var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, options);
// 通過流對象生成相關的程序集
using MemoryStream stream = new MemoryStream();
//編譯
var result = compilation.Emit(stream);
if (result.Success)
{
// 編譯成功 返回 assembly
stream.Seek(0, SeekOrigin.Begin);
return Assembly.Load(stream.ToArray());
}
else
{
StringBuilder stringBuilder = new StringBuilder();
// 獲取異常
foreach (Diagnostic codeIssue in result.Diagnostics)
{
stringBuilder.Append($"ID: {codeIssue.Id}," +
$" Message: {codeIssue.GetMessage()}," +
$"Location:{codeIssue.Location.GetLineSpan()}," +
$"Severity:{codeIssue.Severity}---");
// AppRuntimes.Instance.Loger.Error("自動編譯代碼出現異常," + msg);
}
throw new Exception(stringBuilder.ToString());
}
}
public string DynamicGenerateDllToFile(string text, string assemblyName = null,params Assembly[] assemblies)
{
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var syntaxTrees = new SyntaxTree[] { CSharpSyntaxTree.ParseText(text) };
if (assemblyName == null)
assemblyName = "_" + Guid.NewGuid().ToString("D");
var references = assemblies.Select(i => MetadataReference.CreateFromFile(i.Location));
var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, options);
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Proxy");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
var apiRemoteProxyDllFile = Path.Combine(path,
assemblyName + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".dll");
var result = compilation.Emit(apiRemoteProxyDllFile);
if (result.Success)
{
return apiRemoteProxyDllFile;
}
else
{
StringBuilder stringBuilder = new StringBuilder();
// 獲取異常
foreach (Diagnostic codeIssue in result.Diagnostics)
{
stringBuilder.Append($"ID: {codeIssue.Id}," +
$" Message: {codeIssue.GetMessage()}," +
$"Location:{codeIssue.Location.GetLineSpan()}," +
$"Severity:{codeIssue.Severity}---");
// AppRuntimes.Instance.Loger.Error("自動編譯代碼出現異常," + msg);
}
throw new Exception(stringBuilder.ToString());
}
}
}
}
// 從生成號的 assembly 中加載 controller
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace DynamicRegister1
{
/*由於針對MVC應用的請求總是指向某一個Action,
* 所以MVC框架提供的路由整合機制體現在為每一個Action創建一個或者多個終結點
* (同一個Action方法可以注冊多個路由)。
* 針對Action方法的路由終結點是根據描述Action方法的ActionDescriptor對象構建而成的。
* 至於ActionDescriptor對象,則是通過注冊的一組IActionDescriptorProvider對象來提供的,
* 那么我們的問題就迎刃而解:通過注冊自定義的IActionDescriptorProvider從動態定義的Controller
* 類型中解析出合法的Action方法,並創建對應的ActionDescriptor對象*/
public class OwnActionProvider : IActionDescriptorProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly ICompiler _compiler;
public OwnActionProvider(IServiceProvider serviceProvider, ICompiler compiler)
{
_serviceProvider = serviceProvider;
_compiler = compiler;
_actions = new List<ControllerActionDescriptor>();
}
public int Order => -1000;
/// <summary>
/// 當IChangeToken 發生變化,會執行下面兩個監聽方法
/// </summary>
/// <param name="context"></param>
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
{
// 添加action ,添加完成之后,會將 context.Results中的action更新到 ActionDescriptorCollection,見下面源代碼
foreach (var action in _actions)
{
context.Results.Add(action);
}
}
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
{
}
private readonly List<ControllerActionDescriptor> _actions;
/// <summary>
/// 創建 ApplicationModel
/// 那么ActionDescriptor如何創建呢?我們能想到簡單的方式是調用如下這個Build方法。
/// 針對該方法的調用存在兩個問題:
/// 第一ControllerActionDescriptorBuilder是一個內部(internal)類型,
/// 我們指定以反射的方式調用這個方法,
/// 第二,這個方法接受一個類型為ApplicationModel的參數。
/// </summary>
/// <param name="controllerTypes"></param>
/// <returns></returns>
ApplicationModel CreateApplicationModel(IEnumerable<Type> controllerTypes)
{
// var s = Microsoft.AspNetCore.Mvc.ApplicationModels.AttributeRouteModel;
// 因為ApplicationModelFactory 是內建類型,只能通過反射去獲取對象 創建 ApplicationModel
var assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModelFactory";
var factoryType = assembly.GetTypes().Single(it => it.FullName == typeName);
//IServiceProvider通過GetService(Type serviceType)拿到實例
var factory = _serviceProvider.GetService(factoryType);
var method = factoryType.GetMethod("CreateApplicationModel");
var typeInfos = controllerTypes.Select(it => it.GetTypeInfo());
//通過反射創建 ApplicationModel
return (ApplicationModel)method.Invoke(factory, new object[] { typeInfos });
}
/// <summary>
/// 生成 ControllerActionDescriptor
/// </summary>
/// <param name="sourceCode"></param>
/// <returns></returns>
IEnumerable<ControllerActionDescriptor> CreateActionDescrptors(string sourceCode)
{
Func<Type, Boolean> IsController = new Func<Type, bool>(typeInfo =>
{
if (!typeInfo.IsClass) return false;
if (typeInfo.IsAbstract) return false;
if (!typeInfo.IsPublic) return false;
if (typeInfo.ContainsGenericParameters) return false;
if (typeInfo.IsDefined(typeof(NonControllerAttribute))) return false;
if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && !typeInfo.IsDefined(typeof(ControllerAttribute))) return false;
return true;
});
// 生成assemble
var assembly = _compiler.Compile(sourceCode, null,
Assembly.Load(new AssemblyName("System.Runtime")),
typeof(object).Assembly,
typeof(ControllerBase).Assembly,
typeof(Controller).Assembly);
//獲取程序集中 類型為控制器的類型
var controllerTypes = assembly.GetTypes().Where(it => IsController(it));
// 創建 ApplicationModel,
var applicationModel = CreateApplicationModel(controllerTypes);
assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerActionDescriptorBuilder";
// 反射獲取 ControllerActionDescriptorBuilder 對象
var controllerBuilderType = assembly.GetTypes().Single(it => it.FullName == typeName);
// 反射獲取Build 方法
var buildMethod = controllerBuilderType.GetMethod("Build", BindingFlags.Static | BindingFlags.Public);
// 創建 ControllerActionDescriptor 對象
return (IEnumerable<ControllerActionDescriptor>)buildMethod.Invoke(null, new object[] { applicationModel });
}
public void AddControllers(string sourceCode) => _actions.AddRange(CreateActionDescrptors(sourceCode));
/// <summary>
/// 刪除注冊的controller
/// </summary>
/// <param name="controllerName"></param>
/// <param name="actionName"></param>
public void RemoveControllers(string controllerName, string actionName)
{
for (int i = 0; i < _actions.Count(); i++)
{
if (_actions[i].ActionName == actionName && _actions[i].ControllerName == controllerName)
_actions.RemoveAt(i);
}
}
public IEnumerable<ControllerActionDescriptor> ControllerActionDescriptor
{
get => _actions;
}
}
/// <summary>
/// DynamicActionProvider 解決了將提供的源代碼向對應ActionDescriptor列表的轉換,
/// 但是MVC默認情況下對提供的ActionDescriptor對象進行了緩存。如果框架能夠使用新的ActionDescriptor對象,
/// 需要告訴它當前應用提供的ActionDescriptor列表發生了改變,
/// 而這可以利用自定義的IActionDescriptorChangeProvider來實現。
/// 為此我們定義了如下這個DynamicChangeTokenProvider類型,
/// 該類型實現了IActionDescriptorChangeProvider接口,
/// 並利用GetChangeToken方法返回IChangeToken對象通知MVC框架當前的ActionDescriptor已經發生改變。
/// 從實現實現代碼可以看出,當我們調用NotifyChanges方法的時候,狀態改變通知會被發出去。
/// </summary>
public class OwnChangeTokenProvider : IActionDescriptorChangeProvider
{
private CancellationTokenSource _source;
private CancellationChangeToken _token;
public OwnChangeTokenProvider()
{
_source = new CancellationTokenSource();
_token = new CancellationChangeToken(_source.Token);
}
/// <summary>
/// 當token 發生變化時,會調用 IActionDescriptorProvider
/// 中的OnProvidersExecuted,OnProvidersExecuting兩個監聽方法
/// </summary>
/// <returns></returns>
public IChangeToken GetChangeToken() => _token;
public void NotifyChanges()
{
// 給_source 賦值,並獲取舊值
var old = Interlocked.Exchange(ref _source, new CancellationTokenSource());
_token = new CancellationChangeToken(_source.Token);
// 取消舊的注冊
old.Cancel();
}
}
#region 以下是更新ActionDescriptorCollection的源代碼
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private ActionDescriptorCollection _collection;
private int _version = -1;
/// <summary>
/// Initializes a new instance of the <see cref="ActionDescriptorCollectionProvider" /> class.
/// </summary>
/// <param name="actionDescriptorProviders">The sequence of <see cref="IActionDescriptorProvider"/>.</param>
/// <param name="actionDescriptorChangeProviders">The sequence of <see cref="IActionDescriptorChangeProvider"/>.</param>
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);
}
/// <summary>
/// Returns a cached collection of <see cref="ActionDescriptor" />.
/// </summary>
public ActionDescriptorCollection ActionDescriptors
{
get
{
if (_collection == null)
{
UpdateCollection();
}
return _collection;
}
}
private void UpdateCollection()
{
var context = new ActionDescriptorProviderContext();
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
{
_actionDescriptorProviders[i].OnProvidersExecuting(context);
}
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
{
_actionDescriptorProviders[i].OnProvidersExecuted(context);
}
// 返回 ActionDescriptorCollection
_collection = new ActionDescriptorCollection(
new ReadOnlyCollection<ActionDescriptor>(context.Results),
Interlocked.Increment(ref _version)); // 原子操作
}
}
#endregion
}
效果:

訪問注冊的方法

二 實現方式2
參考:https://stackoverflow.com/questions/46156649/asp-net-core-register-controller-at-runtime
ApplicationPart 是用來管理運行時中加載的 dll 的,
只要能把帶有Controller的dll 加載到ApplicationParts,刷新一下相關的 runtime 就能實現了吧。有了思路,就先看看 .NET Core MVC 源碼吧,從里面找找看有沒有相關的 Controller 緩存的集合,看能不能動態加載進去。
public IActionResult Index(string source,
[FromServices] ApplicationPartManager manager,
[FromServices] ICompiler compiler,
[FromServices] OwnChangeTokenProvider tokenProvider)
{
try
{
manager.ApplicationParts.Add(new AssemblyPart(compiler.Compile(source,null, Assembly.Load(new AssemblyName("System.Runtime")),
typeof(object).Assembly,
typeof(ControllerBase).Assembly,
typeof(Controller).Assembly)));
tokenProvider.NotifyChanges();
return Content("OK");
}
catch (Exception ex)
{
return Content(ex.Message);
}
}
