網站首頁 編程語言 正文
一些動態生成controller的問題
前言
最近在寫包, 一開始封裝了倉儲Repository
用于操作數據庫, 然后為了快速開發一些業務簡單的接口, 通過QueryController
, ModifyController
, CrudController
提供默認實現, 在添加接口的時候只需要新建一個 Controller, 然后繼承
public class TestController : QueryRepController<int?, TestEntity, TestEntityGet> { public TestController(IQueryRepository<int?, TestEntity> repository) : base(repository) { } }
即可實現簡單的增刪改查功能
看到 TestController
這單薄的實現, 我突然有個想法
"既然這個controller寫得這么簡單, 為什么我不能嘗試靠代碼去生成呢!?!"
雖然這個功能不一定有什么用, 但我還是開始了踩坑
動態新建Type
經過簡單的思考, 我認為第一步應該是創建 Type
嘗試的方案一
最開始嘗試注冊一堆 typeof(QueryRepController<int?, TestEntity, TestEntityGet>)
, 然后動態創建路由
但我搞了半天也沒發現asp.net里面有相關的功能, 也不能確定這樣生成的 Type
是正常的, 感覺這里面能讓我栽進去的坑有很多
雖然可以自己重新實現一套路由......后面還得搞日志, 攔截器什么的 ?!?
我廢那勁干嘛, 于是放棄
嘗試的方案二
之前就聽說C#有 Source Generator, 可以在編譯時直接生成代碼
還聽說 AutoMapper
就用了這種技術(也不知道是真是假)
然后決定研究一下......
一個周末的時間讓我了解到, 這東西好像沒多少人用啊, 相關資料少得可憐, 網上逛了兩天, 除了說這東西很有用, 很香, 沒找著多少對我有用的資料, 也可能是我太菜了不會用
雖然最后生成了一個可以正常使用的 Controller
, 但是與我的預期有極大的差距
我期望的使用方式類似下面這種
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test");
在使用的時候可以主動通過注冊的方式添加 Controller, 然后可以自由更改路由(比如把Test改為WTF)
搞了兩天感覺方向不對, 雖然 Source Generator 確實挺有意思的, 也有可以發揮的場景, 但至少不太符合我這時的需要
嘗試的方案三
從 Source Generator 中抽身后, 我又開始大海撈針式地尋找方案
然后在 萬能的stackoverflow 上找到了可能的方案
使用Emit擼IL
說實話在這之前我從來沒有聽說過 dotnet 中的 Emit
, 平時使用的反射也只是 GetValue
SetValue
這樣的, 這鬼東西真是讓我大開眼界。
經過一番"艱苦"奮戰后, 磕磕絆絆憋出了類似下面的代碼
public static IServiceCollection AddQueryRepController<TKey, T, GetT>(this IServiceCollection services, string route) where T : class, IBaseEntity<TKey> where GetT : IBaseGet<T> { // 建一個 Assembly AssemblyBuilder Ass = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("NewController"), AssemblyBuilderAccess.Run); ModuleBuilder MB = Ass.DefineDynamicModule("NewController"); // 起個好聽的名字 var typeName = $"{route}Controller"; // 使用QueryRepController<TKey, T, GetT>整一個builder var typeBuilder = MB.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, typeof(QueryRepController<TKey, T, GetT>), null); // 添加一個構造函數, var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(IQueryRepository<TKey, T>) }); // 給這個構造函數編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); // 創建這個新的 type var type = typeBuilder.CreateType(); // 根據自己的情況注冊到容器中 services.AddTransient(typeof(IQueryController<TKey, T, GetT>), type); return services; }
以我的水平和能力, 做到這樣已經是極限, 靠ILSpy反編譯上面的 TestController
, 抄了點代碼(我抄我自己)
現在可以使用
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test")
生成并注冊一個 TestController 到容器中, 也可以正常獲取實例
但是程序就是無法感知到代碼的變化, swagger 中也看不到新加的 Controller
嘗試進行請求, 最后也以 404 Not Found
失敗告終
于是再次陷入僵局
使用ApplicationPartManager注冊controller
之前在逛園子的時候看到 Artech大佬的 文章 , 當時看的時候感覺云里霧里的, 不知所云
也嘗試硬著頭皮寫, 但是沒有能夠堅持下去, 但我在完成以上步驟并且被卡住后, 再次看了大佬的文章, 豁然開朗!
為了讓這些程序集成為應用的一個有效組成部分,程序集需要封裝成ApplicationPart對象并利用ApplicationPartManager進行注冊
參考大佬的文章, 寫了如下的實現
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); } }
又有一個 HostedService
在注冊完成后通過 ApplicationPartManager
更新注冊信息
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) }
之后使用時注冊 AddControllerChangeProvider
和 ChangeActionService
services.AddSingleton<IActionDescriptorChangeProvider>(AddControllerChangeProvider.Instance); services.AddHostedService<ChangeActionService>();
程序運行后會啟動 ChangeActionService
, 讀取我之前生成controller時使用的 AssemblyBuilder, 注冊生成的新的controller
這時就已經可以在 swagger 中看到創建的 TestController 了, 并且也能正常進行訪問
最后貼一下代碼
之后經過一系列過度封裝, 簡單的代碼如下(用了很多自己的封裝, 看看就好...)
var builder = WebApplication.CreateBuilder(args); builder.Services.AddMysql<TestDbContext>("localhost", 3306, "test", "root", "pwd") // 將 TestDbContext 注冊為默認的 DbContext .AddDefaultDbContext<TestDbContext>() .AddControllers(); builder.Services // 注冊一個 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) { } } // 對應數據庫中的 Test 表 public class TestEntity : BaseEntity<long?> public string Code { get; set; } public int? Number { get; set; } public bool? IsTest { get; set; } // 對應 TestEntity 的 TestEntityGet, 決定接口的查詢規則 public class TestEntityGet : BaseGet<TestEntity> public string? Code { get; set; }
雖然沒啥卵用, 但是寫出這段代碼的那一刻, 我自己是爽了, 有沒有用已經不重要的
原文鏈接:https://www.cnblogs.com/CollapseNav/p/16027345.html
- 上一篇:go語言實現兩個協程交替打印_Golang
- 下一篇:C語言實現數獨小游戲_C 語言
相關推薦
- 2023-01-15 PyQt5+QtChart繪制散點圖_python
- 2022-06-06 詳解如何自定義Dubbo Filter(含dubbo2.7.X及以上版本和2.6.X及以下版本兩種寫
- 2022-12-26 C++逆向分析移除鏈表元素實現方法詳解_C 語言
- 2022-04-23 npm publish 組件流程以及報錯總結
- 2023-07-25 mybatis-plus在實際開發中的應用
- 2023-10-24 記ElementUI內置的$confirm確認消息彈框方法
- 2022-10-03 Go?Excelize?API源碼閱讀Close及NewSheet方法示例解析_Golang
- 2023-12-09 springframework.jdbc.BadSqlGrammarException:
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支