日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

.Net動態生成controller遇到的坑_實用技巧

作者:崩壞的領航員 ? 更新時間: 2022-05-27 編程語言

一些動態生成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)
}

之后使用時注冊 AddControllerChangeProviderChangeActionService

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

欄目分類
最近更新