☰
Blazor Admin 请求到底是怎么跑起来的?从页面到数据库的完整调用链
2026/10/8 2:03:22 网站建设 项目流程

本文对应版本:EasyAdminBlazor 2.3.0。

源码位置:

  • EasyAdminBlazor/AdminExtensions.cs(注册与中间件)
  • EasyAdminBlazor/AdminContext.cs及 8 个 partial:Auth/Authorization/Cookie/Favorites/Navigation/Tenant/User
  • EasyAdminBlazor/Components/AdminTable.razor、Components/Layout/MainLayout.razor(.cs)
  • EasyAdminBlazor.Core/(实体与抽象)、Extensions/(可选扩展)
  • EasyAdminBlazor.Test/Program.cs(宿主接入)

已发布的《从项目结构到核心架构》讲的是目录和分层。这篇讲运行时的调用链:一次页面访问、一次表格查询、一次保存,代码到底经过了哪些地方。


一、先看依赖方向

EasyAdminBlazor.Test 宿主项目(Program.cs + 实体 + 页面) │ 引用 ▼ EasyAdminBlazor 主包(组件 + AdminContext + 文件/日志/安全) │ 引用 ▼ EasyAdminBlazor.Core 实体 + 抽象接口(ICacheService / ITenantService / ISchedulerService / IApprovalGateway / SecurityOptions …) Extensions/* 可选扩展,反向依赖主包,通过 DI 替换抽象实现

关键点:扩展是"替换实现"而不是"改主包代码"。核心包里注册的都是空实现或默认实现:

// AdminExtensions.csbuilder.Services.TryAddSingleton<ITenantService,NullTenantService>();builder.Services.TryAddSingleton<ICacheService,MemoryCacheService>();builder.Services.TryAddSingleton<ISchedulerService,NullSchedulerService>();builder.Services.TryAddScoped<IApprovalGateway,NullApprovalGateway>();builder.Services.TryAddTransient<IEditorComponent,DefaultEditorPlugin>();builder.Services.TryAddTransient<IChatComponent,DefaultChatPlugin>();builder.Services.TryAddTransient<IEmailService,DefaultEmailService>();builder.Services.TryAddSingleton<ISmsService,DefaultSmsService>();

扩展则用TryAdd*/RemoveAll覆盖它们(例:AddEasyAdminBlazorMultiTenant会RemoveAll<ITenantService>()再注册MultiTenantService)。

这是整个框架能"按需安装"的根本原因:主包只依赖接口,扩展提供实现。


二、启动期:AddEasyAdminBlazor 做了四件事

1. 建主库 ORM 并初始化种子数据

varmainBuilder=newFreeSqlBuilder().UseNoneCommandParameter(true);if(options.FreeSqlBuilder!=null)options.FreeSqlBuilder.Invoke(mainBuilder);elsemainBuilder.UseConnectionString(DataType.Sqlite,@"Data Source=master.db").UseMonitorCommand(cmd=>System.Console.WriteLine(...)).UseAutoSyncStructure(true);varfsql=mainBuilder.Build();FreeSqlConfiguration.Configure(fsql);SeedData.InitSeedData(fsql);builder.Services.AddSingleton(newMainOrmHandle(fsql));

注意new MainOrmHandle(fsql)注册为Singleton,这是多租户能成立的前提:AdminContext需要同时持有"主库引用"和"当前租户库引用"。

2. 注册仓储与审计

// 用户部门if(e.AuditValueType==AuditValueType.Insert&&e.ObjectisIDataPermissionobj3&&obj3!=null){obj3.OrgId=user.OrgId;return;}
builder.Services.AddScoped(typeof(IBaseRepository<>),typeof(BasicRepository<>));builder.Services.AddScoped(typeof(IAggregateRootRepository<>),typeof(DddRepository<>));

DddRepository.Select返回base.SelectDiy,所以AdminTable注入IAggregateRootRepository<TItem>后能直接拿到ISelect<TItem>。

3. 注册基础设施

类别注册内容
认证自定义登录票据认证处理器 + 默认认证方案(仅在宿主未指定时生效)
CSRFAddAntiforgery+EasyAdminAntiforgeryFilter
实时AddSignalR(KeepAlive 15s / ClientTimeout 3min)+ Circuit 保留配置
缓存TryAddSingleton<ICacheService, MemoryCacheService>()
日志DatabaseLoggerQueue+DatabaseLoggerBackgroundService+DatabaseLoggerProvider
本地化AddRequestLocalization(Cookie + Accept-Language)
服务OperationLogService/PrintService/AdminMessageService/FileService
后台上下文AddScoped<AdminContext>()

4. 映射 Hub

publicstaticWebApplicationUseEasyAdminBlazor(thisWebApplicationapp){// 映射 SignalR 通知 Hubapp.MapHub<NotificationHub>("/hubs/notification");returnapp;}

UseEasyAdminBlazor()只做这一件事——这也是它必须在app.MapRazorComponents<App>()之后调用的原因。


三、请求期:一次后台页面访问

以"打开产品管理页"为例:

浏览器 GET /admin/xxx/Admin/Product ↓ EasyAdminAuthStartupFilter(中间件) ↓ 校验 AdminRouteSecret,不带正确安全码直接 404 ↓ MapRazorComponents<App>() → Routes → MainLayout ↓ MainLayout.OnInitializedAsync → admin.Init() ↓ 解密登录 Cookie → 加载 SysUser → 检查多端登录 ↓ Layout 的 OnAuthorizing(path) → admin.AuthPath(path) ↓ 加载角色与菜单(含缓存)→ 匹配 PathLower → 设置 CurrentMenu ↓ admin.AuthPathSuccess == true → 渲染 @Body ↓ Product.razor → AdminTable.OnParametersSetAsync ↓ AuthButton("add"/"edit"/"remove") 决定按钮显隐 ↓ OnQueryDataAsync → 查询

登录态是怎么读出来的

publicasyncTaskInit(){if(!string.IsNullOrEmpty(_adminOptions.AdminRouteSecret)){varpath=newUri(nav.Uri).AbsolutePath;if(path.StartsWith("/admin/",StringComparison.OrdinalIgnoreCase)||path.Equals("/admin",StringComparison.OrdinalIgnoreCase)){varsecretCookie=awaitGetCookie($"easyadminblazor_secret_{_adminOptions.CookieName}");if(secretCookie!=_adminOptions.AdminRouteSecret){// 强制刷新页面,触发 EasyAdminAuthStartupFilter 中间件实现 404 拦截nav.NavigateTo(nav.Uri,forceLoad:true);return;}}}varcookie=awaitGetCookie(CookieKey);if(cookie!=null&&TryParseCookie(cookie,outvaruserId,outvarloginTime)&&userId>0){User=awaitOrm.Select<SysUser>().Where(a=>a.Id==userId).FirstAsync();if(User==null){awaitSignOut();RedirectLogin();return;}awaitRemoveCache();// 检查多端登录覆盖if(!awaitCheckOtherLogin(User,loginTime))return;}else{awaitSignOut();}}

三个细节:

  1. AdminRouteSecret是双重校验:中间件层面对/admin/...直接 404;组件层再校验一次 Cookie,缺失就强制整页刷新(回到中间件拦截)。
  2. 登录票据是加密字符串,内容是userId|loginTime:
varencryptedData=_loginTicketProtector.Protect($"{user.Id}|{user.LoginTime.ToString(LoginTimeFormat,CultureInfo.InvariantCulture)}");
  1. loginTime参与多端登录判断:EnableKickOnOtherLogin打开时,后登录会把前一端的LoginTime改掉,前端的CheckOtherLogin发现不一致就踢下线。

四、数据链路:从组件到数据库

AdminTable<TItem> ↓ GetSelect() IAggregateRootRepository<TItem>(DI 注入的 DddRepository) ↓ .Select → base.SelectDiy FreeSql ISelect<TItem> ↓ ApplyDataPermission / WhereDynamicFilter / ApplyOrder / Page SQL → 数据库

AdminContext.Orm是租户感知的入口:

/// <summary>/// 获取 FreeSql 实例(多租户模式下自动切换到租户数据库)。/// </summary>publicIFreeSqlOrm=>Tenant!=null?_tenantService!.GetTenantFreeSql(Tenant.Code):_mainOrmHandle.Orm;

所以"注入IFreeSql"的服务(如FileService、ApprovalService)会自动拿到当前租户库——多租户对业务代码是透明的。


五、Blazor Server 带来的两个特殊处理

1. HttpContext 在 SignalR 回调里可能不可用

AdminContext不缓存HttpContext实例,并且对"WebSocket 请求"单独处理:

publicvoidRedirect(stringurl){varfinalUrl=url;if(_pendingLoginTicket!=null){finalUrl=$"/api/sys/auth/set-cookie?ticket={_pendingLoginTicket}&remember={_pendingLoginRemember}&redirect={Uri.EscapeDataString(url)}";_pendingLoginTicket=null;}...varctx=GetHttpContextSafely();if(ctx==null||IsWebSocketRequest(ctx)||ctx.Response.HasStarted){nav.NavigateTo(finalUrl,true);}else{ctx.Response.Redirect(finalUrl);}}

IsWebSocketRequest()为真时不能用Response.Redirect(响应已经开始/是长连接),改成客户端导航;需要写 Cookie 的操作则先放进_pendingLoginTicket,再由专门的路由(/api/sys/auth/set-cookie)完成。

2. 租户解析要缓存

publicSysTenant?Tenant{get{if(!_tenantResolved){_cachedTenant=_tenantService?.GetCurrentTenant();_tenantResolved=true;}return_cachedTenant;}}

Circuit 生命周期很长,HttpContext却可能已经不可用,所以解析结果要缓存;同时提供InvalidateTenantCache()/SetTenant()让切换与失效可控(第 13 篇)。


六、一次保存的完整链路

编辑弹窗点"保存" ↓ EditContextCapture.Validate() 客户端校验 ↓ OnSaveAsync → AdminTable.OnSaveDataAsync ↓ admin.AuthButton("add"/"edit") 服务端权限校验 ↓ FilterAuthorizedAsync(更新时) 数据权限回查 ↓ OnBeforeSaveAsync 页面业务校验 ↓ ApprovalGateway.CheckModifyAsync 审批中禁止修改 ↓ _repo.InsertAsync / UpdateAsync ↓ AuditValue 填充创建人/时间/OrgId ↓ 提交后:审批自动提交(若配置)+ OnFinishSaveAsync + 清草稿

这条链路里出现的AdminContext、IAggregateRootRepository、IApprovalGateway都是 DI 注入的,所以每个环节都可以被扩展替换(比如自定义IApprovalGateway)。


七、扩展的插入点一览

想改什么替换哪个抽象用哪个扩展/方式
缓存实现ICacheServiceAddEasyAdminBlazorFusionCache()
多租户ITenantServiceAddEasyAdminBlazorMultiTenant()
定时任务ISchedulerServiceAddEasyAdminBlazorScheduler()
审批IApprovalGateway/IApprovalServiceAddEasyAdminBlazorApproval()
富文本IEditorComponentAddEasyAdminBlazorTinyMCEEditor()
聊天IChatComponentAddEasyAdminBlazorChat()
邮件 / 短信IEmailService/ISmsServiceAddEasyAdminBlazorMail()/AddEasyAdminBlazorAliyunSms()
日志落库ILoggerProvider内置DatabaseLoggerProvider(可关闭)

八、一张总图

┌──────────────── 启动期 ────────────────┐ Program.cs → AddEasyAdminBlazor() ├─ 构建主库 FreeSql(MainOrmHandle, Singleton) ├─ SeedData 初始化菜单/角色/管理员 ├─ 注册仓储、审计、缓存、日志队列、本地化 └─ 注册空实现接口(等待扩展替换) app.UseEasyAdminBlazor() → MapHub("/hubs/notification") └─────────────────────────────────────────┘ ┌──────────────── 请求期 ────────────────┐ HTTP 请求 → EasyAdminAuthStartupFilter(AdminRouteSecret / 404) → MainLayout → admin.Init()(票据解密 → SysUser → 多端登录) → admin.AuthPath(path)(角色+菜单 → CurrentMenu) → @Body 渲染 → AdminTable(IsSelect → 数据权限 → 动态筛选 → 分页) → 保存/删除(AuthButton → 数据权限 → 事务/审批) └─────────────────────────────────────────┘ ┌──────────────── 数据层 ────────────────┐ AdminContext.Orm →(有租户 ? 租户库 : 主库)→ FreeSql → 数据库 └─────────────────────────────────────────┘

九、小结

理解这套架构,抓住三句话就够了:

  1. 主包只依赖接口,扩展替换实现——所以功能可以按需安装、按需关闭;
  2. AdminContext是运行时中枢——用户、角色、菜单、配置、租户、导航都在它身上,其余组件围绕它工作;
  3. Blazor Server 没有 HTTP 边界——页面、权限、数据访问都在同一进程里,链路短,但要专门处理 Circuit 带来的"HttpContext 不可用"和"长生命周期缓存"问题。

下一篇会顺着这条链路回答一个常见质疑:既然没有 Controller、Service、DTO,那这套架构是怎么保证可维护性的?


如果你正在用 .NET 10 + Blazor 做后台,想搞清楚"框架到底帮你做了什么",可以直接对着这篇的调用链读源码,每一段都能在仓库里找到对应文件。

  • 文档:https://easyadmin.wang-zhan.com.cn/doc
  • 源码:https://gitee.com/gudufy/EasyAdminBlazor

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询