当项目中实体数量增多时,为每个实体手写 Controller 会产生大量重复代码。本文介绍两种减少重复的方式:泛型基类 Controller 和 动态 Controller 生成。
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 少量实体,每个都有自定义逻辑 | 手写 Controller | 灵活度最高 |
| 实体较多,大部分只需标准 CRUD | 泛型基类 Controller | 减少重复,保留 override 扩展能力 |
| 实体很多,CRUD 模式高度统一 | 动态 Controller 生成 | 零手写,自动扫描实体并生成 |
using static LiteOrm.Common.Expr;
[ApiController]
[Route("api/[controller]")]
public abstract class EntityControllerBase<T, TView> : ControllerBase
where T : class
where TView : class
{
private const int MaxPageSize = 100;
protected IEntityServiceAsync<T> EntityService => HttpContext.RequestServices.GetRequiredService<IEntityServiceAsync<T>>();
protected IEntityViewServiceAsync<TView> ViewService => HttpContext.RequestServices.GetRequiredService<IEntityViewServiceAsync<TView>>();
[HttpGet("{id}")]
public virtual async Task<TView?> GetById(object id)
{
return await ViewService.GetObjectAsync(id);
}
[HttpGet]
public virtual async Task<List<TView>> List()
{
return await ViewService.SearchAsync();
}
[HttpPost]
public virtual async Task<bool> Create(T entity)
{
return await EntityService.InsertAsync(entity);
}
[HttpPut]
public virtual async Task<bool> Update(T entity)
{
return await EntityService.UpdateAsync(entity);
}
[HttpDelete("{id}")]
public virtual async Task<bool> Delete(object id)
{
return await EntityService.DeleteIDAsync(id);
}
[HttpPost("page")]
public virtual async Task<IActionResult> PageQuery([FromBody] JsonElement exprJson)
{
Expr? expr;
try
{
expr = exprJson.Deserialize<Expr>();
}
catch (Exception ex) when (ex is ArgumentException or JsonException)
{
return BadRequest(new { error = "Invalid Expr JSON.", detail = ex.Message });
}
if (expr is null)
return BadRequest(new { error = "Request body must be a valid Expr JSON." });
var validation = ExprValidator.CreateQueryOnly();
if (!validation.VisitAll(expr))
return BadRequest(new { error = "Expr contains disallowed node types.", failedType = validation.FailedExpr?.GetType().Name });
int skip = 0, take = 20;
if (expr is SectionExpr section)
{
skip = Math.Max(0, section.Skip);
take = section.Take;
if (take < 1) take = 20;
if (take > MaxPageSize) take = MaxPageSize;
expr = RebuildSection(section, skip, take);
}
else if (expr is LogicExpr logicExpr)
{
expr = From<TView>().Where(logicExpr).Section(0, take);
}
else
{
expr = From<TView>().Section(0, take);
}
var countExpr = ExtractFilter(expr);
var total = await ViewService.CountAsync(countExpr);
var items = await ViewService.SearchAsync(expr);
return Ok(new { skip, take, total, items });
}
private static SectionExpr RebuildSection(SectionExpr section, int skip, int take)
{
var source = section.Source;
var rebuilt = (source as ISectionAnchor)?.Section(skip, take)
?? From<TView>().Section(skip, take);
return (rebuilt as SectionExpr)!;
}
private static Expr? ExtractFilter(Expr? expr)
{
return expr switch
{
SectionExpr s => ExtractFilter(s.Source),
OrderByExpr o => ExtractFilter(o.Source),
WhereExpr w => w.Where,
LogicExpr l => l,
_ => null
};
}
}
具体实体的 Controller 只需继承并指定泛型参数:
public class UsersController : EntityControllerBase<User, UserView>
{
}
如果某个实体需要自定义行为,可以 override 对应方法:
public class OrdersController : EntityControllerBase<DemoOrder, DemoOrderView>
{
[HttpGet]
public override async Task<List<DemoOrderView>> List()
{
return await ViewService.SearchAsync(o => o.Status == "Pending");
}
}
基类内置 POST /api/{controller}/page 端点,接受 JSON 序列化的 Expr 作为请求体,自动处理分页和总数统计:
提交 LogicExpr(自动包装分页):
POST /api/DemoDepartments/page
Content-Type: application/json
{
"$": "==",
"Left": {"#": "Name"},
"Right": {"@": "IT"}
}
提交完整查询链(含排序和分页):
POST /api/DemoDepartments/page
Content-Type: application/json
{
"$section": {
"$orderby": {
"$where": null,
"Where": {
"$": "==",
"Left": {"#": "Name"},
"Right": {"@": "IT"}
}
},
"OrderBys": [
{"Field": {"#": "Name"}, "Asc": true}
]
},
"Skip": 0,
"Take": 20
}
返回格式:
{
"skip": 0,
"take": 20,
"total": 1,
"items": [...]
}
分页安全限制:take 最大值为 100,超过时自动截断为 100;小于 1 时默认为 20。
Expr 类型校验:使用 ExprValidator.CreateQueryOnly() 验证提交的 Expr,仅允许查询相关的表达式类型,防止注入风险。
可以在基类中添加更多通用方法,例如条件统计等:
using static LiteOrm.Common.Expr;
[HttpGet("count")]
public virtual async Task<int> Count([FromQuery] string? keyword)
{
if (string.IsNullOrEmpty(keyword))
return await ViewService.CountAsync();
return await ViewService.CountAsync(Prop("Name").Contains(keyword));
}
[HttpGet("exists/{id}")]
public virtual async Task<bool> Exists(object id)
{
return await ViewService.ExistsIDAsync(id);
}
当实体数量很多且都遵循相同的 CRUD 模式时,可以通过 System.Reflection.Emit 在运行时动态生成 Controller 程序集,避免为每个实体手写 Controller 类。
首先定义与第 1 节相同的 EntityControllerBase<T, TView>,作为动态 Controller 的父类。
using System.Reflection;
using System.Reflection.Emit;
public static Assembly BuildDynamicControllers(string defaultNamespace)
{
var assemblyName = new AssemblyName("DynamicControllers");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
foreach (var entityType in typeof(ObjectBase).Assembly.GetTypes())
{
if (!entityType.IsSubclassOf(typeof(ObjectBase)) || entityType.IsAbstract)
continue;
if (entityType.Name.EndsWith("View"))
continue;
if (entityType.GetConstructor(Type.EmptyTypes) == null)
continue;
var viewType = typeof(ObjectBase).Assembly.GetType(entityType.FullName + "View");
if (viewType == null || !viewType.IsSubclassOf(entityType))
viewType = entityType;
var controllerName = $"{entityType.Name}Controller";
var existingController = Type.GetType($"{defaultNamespace}.Controllers.{controllerName}");
if (existingController != null)
continue;
var parentType = typeof(EntityControllerBase<,>).MakeGenericType(entityType, viewType);
var typeBuilder = moduleBuilder.DefineType(
$"{defaultNamespace}.Controllers.{controllerName}",
TypeAttributes.Public, parentType);
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);
var il = ctorBuilder.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, parentType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, Type.EmptyTypes));
il.Emit(OpCodes.Ret);
typeBuilder.CreateType();
}
return assemblyBuilder;
}
在 Program.cs 中调用生成方法,并将动态程序集注册到 MVC:
var builder = WebApplication.CreateBuilder(args);
builder.Host.RegisterLiteOrm();
var dynamicAssembly = BuildDynamicControllers("YourApp");
builder.Services
.AddControllers()
.AddApplicationPart(dynamicAssembly);
var app = builder.Build();
app.MapControllers();
app.Run();
动态生成时会按以下规则查找 View 类型:
实体名 + View(如 User → UserView)TView == T)动态生成时会检查是否已存在同名 Controller(通过 Type.GetType 查找)。如果已存在手写 Controller,则跳过该实体。因此可以混合使用:
AssemblyBuilderAccess.Run 生成的程序集无法保存到磁盘,每次启动都会重新生成