关于dotnet动态生成controller的问题
一些动态生成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.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)
{
await Task.CompletedTask;
}
}
之后使用时注册 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; }
public int? Number { get; set; }
public bool? IsTest { get; set; }
}
虽然没啥卵用, 但是写出这段代码的那一刻, 我自己是爽了, 有没有用已经不重要的
自己写包, 最重要的就是让自己开心!
关于dotnet动态生成controller的问题的更多相关文章
- asp.net Mvc 动态创建Controller
有这么个需求,Urls如下: http://localhost:52804 http://localhost:52804/home/test http://localhost:52804/test1 ...
- ABP(现代ASP.NET样板开发框架)系列之20、ABP展现层——动态生成WebApi
点这里进入ABP系列文章总目录 ABP(现代ASP.NET样板开发框架)系列之20.ABP展现层——动态生成WebApi ABP是“ASP.NET Boilerplate Project (ASP.N ...
- ASP.NET Core开发-MVC 使用dotnet 命令创建Controller和View
使用dotnet 命令在ASP.NET Core MVC 中创建Controller和View,之前讲解过使用yo 来创建Controller和View. 下面来了解dotnet 命令来创建Contr ...
- MVC5+EF6 入门完整教程13 -- 动态生成多级菜单
稍微有一定复杂性的系统,多级菜单都是一个必备组件. 本篇专题讲述如何生成动态多级菜单的通用做法. 我们不用任何第三方的组件,完全自己构建灵活通用的多级菜单. 需要达成的效果:容易复用,可以根据mode ...
- Angular 动态生成html中 ng-click无效
bodyApp.controller('customersCtrl', function ($scope, $http, cfpLoadingBar,$compile) { $scope.test = ...
- Core开发-MVC 使用dotnet 命令创建Controller和View
NET Core开发-MVC 使用dotnet 命令创建Controller和View 使用dotnet 命令在ASP.NET Core MVC 中创建Controller和View,之前讲解过使 ...
- ABP展现层——动态生成WebApi
ABP展现层——动态生成WebApi 点这里进入ABP系列文章总目录 ABP(现代ASP.NET样板开发框架)系列之20.ABP展现层——动态生成WebApi ABP是“ASP.NET Boilerp ...
- asp.net mvc 动态编译生成Controller
做网站后台管理系统的时候,有时我们需要根据用户的录入配置动态生成一些频道,这些频道需要用到独立的Controller,这时就需要用到运行时动态编译了.代码如下: using System.Web.Mv ...
- 【转】AngularJS动态生成div的ID
AngularJS动态生成div的ID 原文链接:http://blog.csdn.net/you23hai45/article/details/52348078 1.问题背景 给定一个数组对象,里面 ...
随机推荐
- Python—高级函数
Python-高级函数 一.闭包 Python函数是支持嵌套的.如果在一个内部函数中对外部函数作用域(非全局作用域)的变量进行引用,那么内部函数就会被称为闭包.闭包需要满足如下3个条件: 存在于两个嵌 ...
- 无脑安装——Python 及 安装python集成开发环境pycharm
无脑安装--Python 及安装python集成开发环境pycharm 1.真机安装python 2.安装python集成开发环境pycharm Python 是一种解释型语言 Python 是面向对 ...
- 「BUAA OO Pre」Git生成多个ssh key并连接GitLab仓库
「BUAA OO Pre」Git生成多个ssh key并连接GitLab仓库 Part 0 前言 写作背景 笔者在配置学校GitLab的ssh key时遇到一些问题,原因应为曾经配置过GitHub的s ...
- suse 12 升级 OpenSSH-7.2p2 到 OpenSSH-8.4p1
文章目录 1.查看当前当前环境信息 1.1.查看openssh当前版本 1.2.查看当前linux发行版 2.部署telnet-server 2.1.下载telnet-server 2.2.配置tel ...
- intellij IDEA 安装、简单使用与创建javaWeb项目
这里我用mac for intellij IDEA 进行演示 1.安装 安装的话不多说 与我之前发的webstorm一样 这里提供下intellij IDEA的破解站http://xidea.onl ...
- Seastar 教程(一)
介绍 我们在本文档中介绍的Seastar是一个 C++ 库,用于在现代多核机器上编写高效的复杂服务器应用程序. 传统上,用于编写服务器应用程序的编程语言库和框架分为两个不同的阵营:专注于效率的阵营和专 ...
- NSSCTF-no_wakeup
打开网页是一个派萌的表情包(原神玩家手动狗头) 按照题目的提示点击,出现题目的源码, 观察题目源码,发现就是一个简单的反序列化,这边手打一下php (自己太菜了,枯了) <?phpclass H ...
- HTTP攻击与防范-跨网站脚本攻击
实验目的 1.了解XSS -跨网站脚本攻击带来的危险性. 2.掌握XSS -跨网站脚本攻击的原理与方法 3.掌握防范攻击的方法 实验原理 跨网站脚本攻击之所以会发生,是因为网站的Web应用程序对用户的 ...
- [Java]Thinking in Java 练习2.14
题目 在文档中加入各项的HTML列表. 代码 1 // object/Documentation4.java 2 // TIJ4 Chapter Object, Exercise 14, page 9 ...
- Springboot基于Redisson实现Redis分布式可重入锁【案例到源码分析】
一.前言 我们在实现使用Redis实现分布式锁,最开始一般使用SET resource-name anystring NX EX max-lock-time进行加锁,使用Lua脚本保证原子性进行实现释 ...