EasyAdminBlazor 2.3 源码架构解析:Blazor Admin 请求到底是怎么跑起来的?

本文对应版本:EasyAdminBlazor 2.3.0

源码位置:

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

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


一、先看依赖方向

text 复制代码
EasyAdminBlazor.Test           宿主项目(Program.cs + 实体 + 页面)
        │ 引用
        ▼
EasyAdminBlazor                主包(组件 + AdminContext + 文件/日志/安全)
        │ 引用
        ▼
EasyAdminBlazor.Core           实体 + 抽象接口(ICacheService / ITenantService /
                               ISchedulerService / IApprovalGateway / SecurityOptions ...)

Extensions/*                   可选扩展,反向依赖主包,通过 DI 替换抽象实现

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

csharp 复制代码
// AdminExtensions.cs
builder.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 覆盖它们(例:AddEasyAdminBlazorMultiTenantRemoveAll<ITenantService>() 再注册 MultiTenantService)。

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


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

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

csharp 复制代码
var mainBuilder = new FreeSqlBuilder().UseNoneCommandParameter(true);
if (options.FreeSqlBuilder != null) options.FreeSqlBuilder.Invoke(mainBuilder);
else mainBuilder
    .UseConnectionString(DataType.Sqlite, @"Data Source=master.db")
    .UseMonitorCommand(cmd => System.Console.WriteLine(...))
    .UseAutoSyncStructure(true);

var fsql = mainBuilder.Build();
FreeSqlConfiguration.Configure(fsql);

SeedData.InitSeedData(fsql);

builder.Services.AddSingleton(new MainOrmHandle(fsql));

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

2. 注册仓储与审计

csharp 复制代码
// 用户部门
if (e.AuditValueType == AuditValueType.Insert && e.Object is IDataPermission obj3 && obj3 != null)
{
    obj3.OrgId = user.OrgId;
    return;
}
csharp 复制代码
builder.Services.AddScoped(typeof(IBaseRepository<>), typeof(BasicRepository<>));
builder.Services.AddScoped(typeof(IAggregateRootRepository<>), typeof(DddRepository<>));

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

3. 注册基础设施

类别 注册内容
认证 自定义登录票据认证处理器 + 默认认证方案(仅在宿主未指定时生效)
CSRF AddAntiforgery + 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

csharp 复制代码
public static WebApplication UseEasyAdminBlazor(this WebApplication app)
{
    // 映射 SignalR 通知 Hub
    app.MapHub<NotificationHub>("/hubs/notification");
    return app;
}

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


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

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

text 复制代码
浏览器 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 → 查询

登录态是怎么读出来的

csharp 复制代码
public async Task Init()
{
    if (!string.IsNullOrEmpty(_adminOptions.AdminRouteSecret))
    {
        var path = new Uri(nav.Uri).AbsolutePath;
        if (path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase) ||
            path.Equals("/admin", StringComparison.OrdinalIgnoreCase))
        {
            var secretCookie = await GetCookie($"easyadminblazor_secret_{_adminOptions.CookieName}");
            if (secretCookie != _adminOptions.AdminRouteSecret)
            {
                // 强制刷新页面,触发 EasyAdminAuthStartupFilter 中间件实现 404 拦截
                nav.NavigateTo(nav.Uri, forceLoad: true);
                return;
            }
        }
    }

    var cookie = await GetCookie(CookieKey);
    if (cookie != null && TryParseCookie(cookie, out var userId, out var loginTime) && userId > 0)
    {
        User = await Orm.Select<SysUser>().Where(a => a.Id == userId).FirstAsync();
        if (User == null) { await SignOut(); RedirectLogin(); return; }

        await RemoveCache();

        // 检查多端登录覆盖
        if (!await CheckOtherLogin(User, loginTime)) return;
    }
    else
    {
        await SignOut();
    }
}

三个细节:

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

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

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

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

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

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


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

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

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

csharp 复制代码
public void Redirect(string url)
{
    var finalUrl = url;
    if (_pendingLoginTicket != null)
    {
        finalUrl = $"/api/sys/auth/set-cookie?ticket={_pendingLoginTicket}&remember={_pendingLoginRemember}&redirect={Uri.EscapeDataString(url)}";
        _pendingLoginTicket = null;
    }
    ...

    var ctx = 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. 租户解析要缓存

csharp 复制代码
public SysTenant? Tenant
{
    get
    {
        if (!_tenantResolved)
        {
            _cachedTenant = _tenantService?.GetCurrentTenant();
            _tenantResolved = true;
        }
        return _cachedTenant;
    }
}

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


六、一次保存的完整链路

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

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


七、扩展的插入点一览

想改什么 替换哪个抽象 用哪个扩展/方式
缓存实现 ICacheService AddEasyAdminBlazorFusionCache()
多租户 ITenantService AddEasyAdminBlazorMultiTenant()
定时任务 ISchedulerService AddEasyAdminBlazorScheduler()
审批 IApprovalGateway / IApprovalService AddEasyAdminBlazorApproval()
富文本 IEditorComponent AddEasyAdminBlazorTinyMCEEditor()
聊天 IChatComponent AddEasyAdminBlazorChat()
邮件 / 短信 IEmailService / ISmsService AddEasyAdminBlazorMail() / AddEasyAdminBlazorAliyunSms()
日志落库 ILoggerProvider 内置 DatabaseLoggerProvider(可关闭)

八、一张总图

text 复制代码
                  ┌──────────────── 启动期 ────────────────┐
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 做后台,想搞清楚"框架到底帮你做了什么",可以直接对着这篇的调用链读源码,每一段都能在仓库里找到对应文件。

相关推荐
企业数字化笔记1 小时前
一批相同的电脑怎么建账?数量管理、单件编码和拆分规则
java·后端·电脑
初学AI的小高1 小时前
从LangGraph到DeepAgents:理解AgentHarness
后端·agent
摇滚侠1 小时前
《Spring Boot 3:高级与架构设计》第 1 章 元编程与元信息 个人理解 1
java·spring boot·笔记·后端
对象存储与RustFS2 小时前
跨实现迁移 MinIO→RustFS:mc diff 静默返回才是最容易翻车的一步
后端·rust·开源
吃饱了得干活2 小时前
Java 单点登录实战:一条主线看懂 Session 共享、CAS 与 OAuth2+JWT
java·后端
卷无止境2 小时前
Jev来了,一个不会说话的AI模型正在改写自动化的规则
人工智能·后端
IT_陈寒2 小时前
React的useEffect依赖项居然骗了我三年
前端·人工智能·后端
AINative软件工程2 小时前
LLM 应用的 Bulkhead 隔离工程实践:用舰壁模式防止一个功能的过载拖左整个 AI 系统
后端·llm·ai编程