.Net動(dòng)態(tài)生成controller遇到的坑
一些動(dòng)態(tài)生成controller的問題
前言
最近在寫包, 一開始封裝了倉儲(chǔ)Repository用于操作數(shù)據(jù)庫, 然后為了快速開發(fā)一些業(yè)務(wù)簡單的接口, 通過QueryController , ModifyController , CrudController 提供默認(rèn)實(shí)現(xiàn), 在添加接口的時(shí)候只需要新建一個(gè) Controller, 然后繼承
public class TestController : QueryRepController<int?, TestEntity, TestEntityGet>
{
public TestController(IQueryRepository<int?, TestEntity> repository) : base(repository)
{
}
}即可實(shí)現(xiàn)簡單的增刪改查功能

看到 TestController 這單薄的實(shí)現(xiàn), 我突然有個(gè)想法
"既然這個(gè)controller寫得這么簡單, 為什么我不能嘗試靠代碼去生成呢!?!"
雖然這個(gè)功能不一定有什么用, 但我還是開始了踩坑
動(dòng)態(tài)新建Type
經(jīng)過簡單的思考, 我認(rèn)為第一步應(yīng)該是創(chuàng)建 Type
嘗試的方案一
最開始嘗試注冊(cè)一堆 typeof(QueryRepController<int?, TestEntity, TestEntityGet>), 然后動(dòng)態(tài)創(chuàng)建路由
但我搞了半天也沒發(fā)現(xiàn)asp.net里面有相關(guān)的功能, 也不能確定這樣生成的 Type 是正常的, 感覺這里面能讓我栽進(jìn)去的坑有很多
雖然可以自己重新實(shí)現(xiàn)一套路由......后面還得搞日志, 攔截器什么的 ?!?
我廢那勁干嘛, 于是放棄
嘗試的方案二
之前就聽說C#有 Source Generator, 可以在編譯時(shí)直接生成代碼
還聽說 AutoMapper 就用了這種技術(shù)(也不知道是真是假)
然后決定研究一下......
一個(gè)周末的時(shí)間讓我了解到, 這東西好像沒多少人用啊, 相關(guān)資料少得可憐, 網(wǎng)上逛了兩天, 除了說這東西很有用, 很香, 沒找著多少對(duì)我有用的資料, 也可能是我太菜了不會(huì)用
雖然最后生成了一個(gè)可以正常使用的 Controller, 但是與我的預(yù)期有極大的差距
我期望的使用方式類似下面這種
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test");在使用的時(shí)候可以主動(dòng)通過注冊(cè)的方式添加 Controller, 然后可以自由更改路由(比如把Test改為WTF)
搞了兩天感覺方向不對(duì), 雖然 Source Generator 確實(shí)挺有意思的, 也有可以發(fā)揮的場景, 但至少不太符合我這時(shí)的需要
嘗試的方案三
從 Source Generator 中抽身后, 我又開始大海撈針式地尋找方案
然后在 萬能的stackoverflow 上找到了可能的方案
使用Emit擼IL
說實(shí)話在這之前我從來沒有聽說過 dotnet 中的 Emit, 平時(shí)使用的反射也只是 GetValue SetValue 這樣的, 這鬼東西真是讓我大開眼界。
經(jīng)過一番"艱苦"奮戰(zhàn)后, 磕磕絆絆憋出了類似下面的代碼
public static IServiceCollection AddQueryRepController<TKey, T, GetT>(this IServiceCollection services, string route)
where T : class, IBaseEntity<TKey> where GetT : IBaseGet<T>
{
// 建一個(gè) Assembly
AssemblyBuilder Ass = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("NewController"), AssemblyBuilderAccess.Run);
ModuleBuilder MB = Ass.DefineDynamicModule("NewController");
// 起個(gè)好聽的名字
var typeName = $"{route}Controller";
// 使用QueryRepController<TKey, T, GetT>整一個(gè)builder
var typeBuilder = MB.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, typeof(QueryRepController<TKey, T, GetT>), null);
// 添加一個(gè)構(gòu)造函數(shù),
var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(IQueryRepository<TKey, T>) });
// 給這個(gè)構(gòu)造函數(shù)編IL
var ilGenerator = ctor.GetILGenerator();
// 通過ILSpy反編譯,然后抄il
ilGenerator.Emit(OpCodes.Ldarg, 0);
ilGenerator.Emit(OpCodes.Ldarg, 1);
ilGenerator.Emit(OpCodes.Call, typeof(QueryRepController<TKey, T, GetT>).GetConstructors()[0]);
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);
// 創(chuàng)建這個(gè)新的 type
var type = typeBuilder.CreateType();
// 根據(jù)自己的情況注冊(cè)到容器中
services.AddTransient(typeof(IQueryController<TKey, T, GetT>), type);
return services;
}
以我的水平和能力, 做到這樣已經(jīng)是極限, 靠ILSpy反編譯上面的 TestController, 抄了點(diǎn)代碼(我抄我自己)
現(xiàn)在可以使用
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test")生成并注冊(cè)一個(gè) TestController 到容器中, 也可以正常獲取實(shí)例
但是程序就是無法感知到代碼的變化, swagger 中也看不到新加的 Controller
嘗試進(jìn)行請(qǐng)求, 最后也以 404 Not Found 失敗告終
于是再次陷入僵局
使用ApplicationPartManager注冊(cè)controller
之前在逛園子的時(shí)候看到 Artech大佬的 文章 , 當(dāng)時(shí)看的時(shí)候感覺云里霧里的, 不知所云
也嘗試硬著頭皮寫, 但是沒有能夠堅(jiān)持下去, 但我在完成以上步驟并且被卡住后, 再次看了大佬的文章, 豁然開朗!
為了讓這些程序集成為應(yīng)用的一個(gè)有效組成部分,程序集需要封裝成ApplicationPart對(duì)象并利用ApplicationPartManager進(jìn)行注冊(cè)
參考大佬的文章, 寫了如下的實(shí)現(xiàn)
AddControllerChangeProvider
public class AddControllerChangeProvider : IActionDescriptorChangeProvider
{
public static AddControllerChangeProvider Instance { get; } = new AddControllerChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}又有一個(gè) HostedService 在注冊(cè)完成后通過 ApplicationPartManager 更新注冊(cè)信息
ChangeActionService
public class ChangeActionService : IHostedService
{
private readonly ApplicationPartManager Part;
public ChangeActionService(IServiceScopeFactory scope)
{
Part = scope.CreateScope().ServiceProvider.GetService<ApplicationPartManager>();
}
public async Task StartAsync(CancellationToken cancellationToken)
Part.ApplicationParts.Add(new AssemblyPart( <可以直接使用之前的AssemblyBuilder> ));
AddControllerChangeProvider.Instance.HasChanged = true;
AddControllerChangeProvider.Instance.TokenSource.Cancel();
await Task.CompletedTask;
public async Task StopAsync(CancellationToken cancellationToken)
}之后使用時(shí)注冊(cè) AddControllerChangeProvider 和 ChangeActionService
services.AddSingleton<IActionDescriptorChangeProvider>(AddControllerChangeProvider.Instance); services.AddHostedService<ChangeActionService>();
程序運(yùn)行后會(huì)啟動(dòng) ChangeActionService, 讀取我之前生成controller時(shí)使用的 AssemblyBuilder, 注冊(cè)生成的新的controller
這時(shí)就已經(jīng)可以在 swagger 中看到創(chuàng)建的 TestController 了, 并且也能正常進(jìn)行訪問
最后貼一下代碼
之后經(jīng)過一系列過度封裝, 簡單的代碼如下(用了很多自己的封裝, 看看就好...)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMysql<TestDbContext>("localhost", 3306, "test", "root", "pwd")
// 將 TestDbContext 注冊(cè)為默認(rèn)的 DbContext
.AddDefaultDbContext<TestDbContext>()
.AddControllers();
builder.Services
// 注冊(cè)一個(gè) TestController
.AddQueryRepController<long?, TestEntity, TestEntityGet>("Test")
// 帶注釋的 Swagger
.AddSwaggerWithComments();
var app = builder.Build();
app.UseSwagger().UseSwaggerUI();
app.MapControllers();
app.Run();
public class TestDbContext : DbContext
{
public DbSet<TestEntity> Tests { get; set; }
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{ }
}
// 對(duì)應(yīng)數(shù)據(jù)庫中的 Test 表
public class TestEntity : BaseEntity<long?>
public string Code { get; set; }
public int? Number { get; set; }
public bool? IsTest { get; set; }
// 對(duì)應(yīng) TestEntity 的 TestEntityGet, 決定接口的查詢規(guī)則
public class TestEntityGet : BaseGet<TestEntity>
public string? Code { get; set; }雖然沒啥卵用, 但是寫出這段代碼的那一刻, 我自己是爽了, 有沒有用已經(jīng)不重要的
到此這篇關(guān)于dotnet動(dòng)態(tài)生成controller的問題的文章就介紹到這了,更多相關(guān)dotnet動(dòng)態(tài)生成controller內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET小結(jié)之MVC, MVP, MVVM比較以及區(qū)別(一)
MVC, MVP和MVVM都是用來解決界面呈現(xiàn)和邏輯代碼分離而出現(xiàn)的模式。以前只是對(duì)它們有部分的了解,沒有深入的研究過,對(duì)于一些里面的概念和區(qū)別也是一知半解?,F(xiàn)在一邊查資料,并結(jié)合自己的理解,來談一下對(duì)于這三種模式思想的理解,以及它們的區(qū)別。歡迎各位高手拍磚。2014-05-05
.NET中可空值類型【Nullable<T>】實(shí)現(xiàn)原理
本文主要介紹了.NET中可空值類型的實(shí)現(xiàn)原理,具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-03-03
通過.NET 6實(shí)現(xiàn)RefreshToken
當(dāng)獲取到的Token過期以后,我們必須要重新請(qǐng)求認(rèn)證接口以獲取新的Token,為了提升用戶體驗(yàn),我們一般會(huì)利用Refresh Token功能,本文將具體為大家介紹一下如何實(shí)現(xiàn)Refresh Token,感興趣的可以學(xué)習(xí)一下2022-01-01
使用正則Regex來移除網(wǎng)頁的EnableViewState實(shí)現(xiàn)思路及代碼
創(chuàng)建好網(wǎng)頁時(shí),什么都沒有寫,但運(yùn)行時(shí)會(huì)發(fā)現(xiàn)源程序(View Source),下面一段,此刻,也許你會(huì)想起,在網(wǎng)頁有一個(gè)屬性EnableViewState,在某些時(shí)候我們并不需要它,接下來將介紹如何移除它,感興趣的朋友可以了解下啊2013-01-01
Asp.Net MVC4通過id更新表單內(nèi)容的思路詳解
一個(gè)表單一旦創(chuàng)建完,其中大部分的字段便不可再編輯。只能編輯其中部分字段。下面通過本文給大家分享Asp.Net MVC4通過id更新表單內(nèi)容的思路詳解,需要的朋友參考下吧2017-07-07
創(chuàng)建基于ASP.NET的SMTP郵件服務(wù)的具體方法
Asp.net在System.Web.Mail名稱空間中有一個(gè)發(fā)送email的內(nèi)建類,但這僅是cdosys的一個(gè)假象。開發(fā)者能使用一個(gè)替代的它smtp郵件服務(wù)。在這篇文章里面,我將會(huì)展示如何創(chuàng)建一個(gè)用于asp.net的功能齊全的smtp郵件服務(wù)2013-11-11

