1. 先说结论:Blazor 和 Web API 当然能共存
1.1 为什么会有这个疑问
做了几年 XAF(DevExpress eXpressApp Framework)开发的朋友,应该都遇到过类似的对话:项目里已经跑起来一套 XAF Blazor 应用,业务对象、权限、审计日志都在里面,突然产品提了个需求,说要给移动端 App 提供数据接口,或者让第三方系统拉取订单信息。这时候第一反应往往是“要不要单独建一个 Web API 项目?”紧接着第二反应就是——“Blazor 和 API 到底能不能共存?是不是同一个应用里只能选一个?”
这里我可以直接给答案:能共存,而且共存得很干净。Blazor Server 的交互模式虽然看起来是“页面里到处是 SignalR 推送”,但它本质上是 ASP.NET Core 应用里的一个端点(Endpoint),不是整个应用的全部。一个 ASP.NET Core 进程里,你可以同时拥有 Razor 页面、MVC Controller、最小 API 和 Blazor 组件,它们共享同一个 HTTP 管线,只是路由各管一段。XAF Blazor 同样如此,它只是把你的业务模型托在 Blazor 的壳里,底层依然是标准 ASP.NET Core,完全可以再挂一层 Web API。
1.2 在 XAF Blazor 里挂 API 的典型场景
我梳理了实际项目中最常见的几种需求,你可以对照看看自己属于哪一种。
第一类,移动端或外部系统需要访问业务数据。比如 OA 系统要显示项目进度,但数据源在 XAF 应用里;或者你写了一个小程序,需要登录后查询客户列表。这时候 API 是唯一合理方式,总不能叫第三方直接连数据库读表,那样等于把数据模型和安全策略全暴露了。
第二类,同一应用内的扩展模块。有些功能用 Blazor 组件做非常别扭,比如给运维脚本提供批量导入接口,或者给定时任务提供触发入口。你用 Controller 写一个 10 行的 POST 接口,比在页面里加一个隐藏按钮再靠自动化点击要靠谱得多。
第三类,为前后端分离做铺垫。公司战略上打算将来把 UI 换成桌面端或原生 App,但后端业务规则短期内不想重写。先在现有 XAF Blazor 应用里把 API 稳定下来,UI 层后续随意替换,这是成本最低的演进路径。
2. 方案选型:先别急着多开一个项目
2.1 三种常见做法
集成 Web API 不是只有一条路,我见过不少团队一上来就建独立 API 项目,结果权限规则复制了一遍又一遍,最后维护成本居高不下。这里先把三种主流做法摊开看。
| 方案 | 做法 | 复杂度 | 典型适用场景 |
|---|---|---|---|
| A | 在现有 XAF Blazor 项目里挂 Web API | 低 | 内部接口、小规模对外服务、快速交付 |
| B | 独立 Web API 项目,引用业务类库 | 中 | 团队分工明确,API 需要独立部署扩缩容 |
| C | 使用官方 Web API Service 模板 | 中高 | 从零开始搭建、API 是产品主业务的场景 |
方案 B 的思路是:把业务对象和 XafApplication 代码抽到独立的类库,Blazor 项目引用一份,API 项目引用另一份。好处是 API 可以独立发布,坏处是业务逻辑共享时,你很快会发现“权限规则到底写在哪边”成了一个哲学问题,而且每次业务模型变更,两边都要重新部署验证。
方案 C 是 DevExpress 官方提供的 Web API Service 模板,适合全新项目。但如果你的业务逻辑已经从 Blazor 项目里长出来了,强行迁移到独立模板,改动面并不小。你不但要重新配置模块列表、数据库更新逻辑,连 ObjectSpaceProvider 的初始化方式可能都要调整。
2.2 为什么优先选方案 A
我的建议是:除非公司明确要求 API 必须部署在单独服务器上,否则优先在当前 Blazor 项目里挂 API。原因很实在。
第一,XAF 的核心价值从来不只是“列表和表单界面”,而是那套业务对象模型、权限体系、审计机制和验证规则。API 如果另起炉灶,等于把业务模型硬生生切开。你写一个查询客户列表的接口很简单,但你得保证这个 API 不会读出没有权限的数据,不会绕过必填字段验证,不会跳过审计记录——这些东西在方案 A 里天然继承。
第二,部署运维省事。一个进程里同时跑 UI 和 API,发布时只发布一个站点,环境变量、数据库连接串都只有一份。我见过独立部署的两个项目因为各自的 appsettings.json 里配置的数据库连接串不一致,排查了整整半天才发现是环境配置漂移。
第三,调试体验好。接口和页面在同一个进程里,你可以在 Controller 里直接打断点,也能看到 XAF 的 ObjectSpace 上下文。如果分开两个项目,来回切换调试端口、同步断点就已经消耗了不少精力。
方案 A 当然也有缺点,比如 API 流量大了之后,Blazor 的 SignalR 长连接可能会和 API 请求争抢线程资源。但这是后续演进要考虑的事,对大多数业务系统来说,按日活几千、API 每秒几十次的负载,一个进程完全顶得住。
3. 实操:把 Web API 挂进现有 XAF Blazor 项目
3.1 安装 NuGet 包与版本锁定
我以当前主流的 DevExpress 版本(v23.x / v24.x,API 命名基本一致)为例。在现有 XAF Blazor 项目里,需要新增两个包:
- DevExpress.ExpressApp.WebApi
- DevExpress.ExpressApp.WebApi.Swashbuckle
第二个包不只是 Swagger 生成器,它内部同时引用了 WebApi 相关依赖,建议两个都装,避免缺依赖。
这里有一个重要提醒:DevExpress 包的版本必须和你项目里其他 DevExpress 包保持一致。如果 Blazor 项目已经在用 23.2.5,那么这个新包也要装 23.2.5。混版本是我见过最常见的集成失败原因之一,编译时可能不报错,运行时会冒出各种离奇异常,比如找不到模块、ObjectSpaceProvider 类型不匹配。批量安装 NuGet 包时注意检查版本号,别手滑升级到 24.x 导致项目整个无法启动。
3.2 Program.cs 中的服务注册
打开现有 XAF Blazor 项目的 Program.cs,在AddXafBlazor旁边增加AddXafWebApi。我贴一段典型配置:
var builder = WebApplication.CreateBuilder(args); // 原有 Blazor 服务 builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); // 原有 XAF Blazor 应用注册 builder.Services.AddXafBlazor(builder.Configuration, options => { options.ApplicationType = typeof(MyBlazorApp.Blazor.BlazorApplication); }); // 新增:Web API 服务注册 builder.Services.AddXafWebApi(builder.Configuration, options => { options.ApplicationType = typeof(MyBlazorApp.Blazor.BlazorApplication); options.Authentication.StandardAuthentication.IsEnabled = true; }); builder.Services.AddAuthentication(); builder.Services.AddAuthorization(); var app = builder.Build();这里的AddXafWebApi会做几件事:注册IXafApplicationFactory服务,读取 XAF 配置中的模块列表,把 WebApi 模块加载进来,并准备标准认证的 Token 端点。它不会帮你注册认证中间件,所以AddAuthentication和AddAuthorization需要显式加上,这两行常常被漏掉,结果一调用[Authorize]就抛异常。
选项里的ApplicationType指向同一款 XafApplication 子类,这是方案 A 的关键。Blazor 页面和 Web API 共用同一个业务应用定义,模块、数据库连接、安全策略都是同一套。
3.3 使用 UseXafWebApi 中间件
app构建完成后,需要调用UseXafWebApi,并确保MapControllers被注册。我给的完整管线参考:
var app = builder.Build(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseXafWebApi(); app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); app.MapBlazorHub(); app.MapControllers(); app.MapFallbackToPage("/_Host"); app.Run();路由为什么能共存?关键在于 ASP.NET Core 的路由系统是按顺序匹配的。MapControllers负责把[ApiController]和[Route]特性标注的类映射到路由表;MapBlazorHub注册的是 SignalR hub 路径;MapFallbackToPage("_Host")是最后一层兜底,专门处理非 API、非 Hub 的浏览器请求。三者各管一段,互不干扰。
有一点容易踩坑:UseXafWebApi建议放在UseAuthentication之前。XAF Web API 的中间件内部要初始化 Token 端点等配置,如果放错了位置,后续请求可能 401。我习惯于把它和UseStaticFiles放在相邻位置,这样意图清晰。
3.4 写第一个 Controller
服务注册和中间件都配好后,在项目里新建一个 Controllers 文件夹,写一个最基础的只读接口。我这里演示一个基于联系人的简单查询:
using DevExpress.ExpressApp.WebApi; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace MyBlazorApp.Blazor.Controllers; [Route("api/[controller]")] [ApiController] [Authorize] public class ContactController : ControllerBase { private readonly IXafApplicationFactory _applicationFactory; public ContactController(IXafApplicationFactory applicationFactory) { _applicationFactory = applicationFactory; } [HttpGet] public IActionResult Get() { var application = _applicationFactory.CreateApplication(); using var objectSpace = application.CreateObjectSpace(typeof(Contact)); var contacts = objectSpace.GetObjects<Contact>().Select(c => new { c.Id, c.FullName, c.Email }).ToList(); return Ok(contacts); } }注意几个细节。
第一,IXafApplicationFactory从构造函数注入,不需要自己new。这个工厂是AddXafWebApi注册的,目的是把 XafApplication 的创建和管理交给容器。你可以理解成:它帮你维护了一个“共享的业务应用实例”,你每次拿它创建 ObjectSpace 来操作数据。
第二,CreateApplication()返回的是 XafApplication 实例,通常由工厂缓存复用,不要轻易用using把它销毁。真正需要释放的是CreateObjectSpace得到的 ObjectSpace,我这里用using var确保请求结束后释放连接资源。如果你在多个接口里频繁创建、销毁 XafApplication,很可能引发性能问题,因为 XafApplication 内部包含模块集合、数据库更新逻辑等重量级组件,设计上并不是为“每请求一实例”准备的。
第三,返回数据用匿名对象或 DTO,不要直接返回实体列表。XAF 的实体对象带有导航属性、延迟加载代理,直接序列化可能触发循环引用,也可能把一堆不该暴露的字段发给前端。
4. 关键机制解读:API 层如何复用 XAF 的能力
4.1 IXafApplicationFactory 在 API 层做了什么
很多第一次接触这个模式的人会问:为什么不直接在 Controller 里 new 一个 XafApplication?原因是 XafApplication 的初始化成本太高。它需要加载模块、解析数据库类型、初始化安全系统、执行DatabaseUpdateMode相关的更新检查。如果每个接口请求都走一遍,应用等于一直处于启动状态,性能惨不忍睹。
IXafApplicationFactory的设计目标就是把这个初始化过程收拢到工厂里,内部缓存 XafApplication 实例。你每次调用CreateApplication(),拿到的很可能是同一个已经初始化好的实例,然后在这个实例上CreateObjectSpace()获取独立的数据上下文。这个设计在 Blazor UI 侧其实也是类似的:XAF Blazor 的中间件在启动时初始化一个应用实例,之后页面操作都围绕这个实例展开。
实操上还有一点:工厂创建的 ObjectSpace 是线程安全的,因为 XAF 的对象空间本身就是为“每用户操作独占上下文”设计的。Controller 是瞬时(scoped)注册的,你完全可以在多个并发请求里各自创建、各自提交,互不干扰。
4.2 ObjectSpace 的获取、提交与释放
对于写操作,核心流程是:创建 ObjectSpace → 查数据或NewObject()→ 修改属性 →CommitChanges()。我举例一个更新接口:
[HttpPut("{id:guid}")] public IActionResult Update(Guid id, [FromBody] ContactUpdateDto dto) { var application = _applicationFactory.CreateApplication(); using var objectSpace = application.CreateObjectSpace(typeof(Contact)); var contact = objectSpace.GetObjectByKey<Contact>(id); if (contact == null) { return NotFound(); } if (!string.IsNullOrWhiteSpace(dto.FullName)) { contact.FullName = dto.FullName; } if (!string.IsNullOrWhiteSpace(dto.Email)) { contact.Email = dto.Email; } objectSpace.CommitChanges(); return Ok(); }这里最关键的一行是objectSpace.CommitChanges()。XAF 的对象空间缓存了所有修改,不调用提交,数据不会落到数据库。这和 Blazor 页面里的ObjectSpace.CommitChanges()一样,只是换成在 Controller 里调用。
另外,我建议在写入接口中用 DTO 而不是直接接受实体 JSON。否则客户端可能把你不想暴露的字段传过来,比如 IsAdmin 这种权限字段直接置 true,造成越权修改。XAF 的权限系统能拦截一部分,但 Controller 层做一次白名单校验更有保障。
4.3 权限和认证怎么打通
这是方案 A 最大的优势:API 和 Blazor 页面共用一套权限体系。
AddXafWebApi选项里开启了Authentication.StandardAuthentication后,会提供一个 Token 端点。客户端拿到 Token,在请求头里带Authorization: Bearer <token>,UseAuthentication中间件验明身份后,XAF 的对象空间会基于当前用户创建安全上下文。也就是说,一个用户如果只被授予了“查看联系人”的权限,那么通过 API 查询联系人时,objectSpace.GetObjects<Contact>()会自动过滤掉他没有权限看到的行,甚至直接抛出安全性异常。
这一点比普通 ASP.NET Core API 多了一层“域权限”屏障。普通 API 的[Authorize]只能校验“你是谁”,XAF 的对象空间还能控制“你能看到哪些数据、改哪些字段”。所以你在 Controller 里写代码时可以很放心,不需要自己重复实现行级权限过滤。
5. 常见问题与避坑清单
5.1 请求 404:Blazor 的 Fallback 路由把 API 吃了?
不少人在集成后遇到一个问题:API 请求返回 404,而页面访问正常。第一反应是“Fallback 路由抢走了 API 请求”,但这其实是误解。
Blazor 的MapFallbackToPage("_Host")是低优先级兜底路由,只有当没有其他路由匹配时才会触发。API 404 的常见原因通常是:
- 忘了在管线里调用
app.MapControllers()。 - Controller 类没有
[ApiController]或[Route]特性,导致它没有被识别为 API 控制器。 - Controller 类不是 public 的,ASP.NET Core 默认不注册非公开类。
- 请求路径写错了,比如路由设计为
api/Contact,但客户端请求了api/contact,大小写不匹配导致 404。
排查路径很直观:用浏览器直接访问 Swagger 地址(通常是{host}/swagger),如果 Swagger 能列出接口,说明 Controller 注册没有问题,问题大概率出在客户端请求路径或认证配置上。
5.2 跨域问题(CORS)
如果你的前端不是 Blazor Server 页面,而是独立的 Vue/React 应用,或者小程序,一定会遇到 CORS 问题。原因是浏览器同源策略限制了跨域请求。
解决方式是在服务注册时添加 CORS 策略:
builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", policy => { policy.WithOrigins("https://your-app-domain.com") .AllowAnyHeader() .AllowAnyMethod(); }); });然后在中间件管线里app.UseCors("AllowSpecificOrigin")。
这里要提醒一个安全细节:不要把WithOrigins("*")写成通用允许,尤其当你的 API 带有认证时。CORS 的*会允许任何来源访问,这对需要 Token 的接口来说,等于把攻击面交给浏览器端任意脚本。我见过内部系统因为图省事配置了AllowAnyOrigin,结果被渗透测试直接判了高危。按域名白名单收口,不用怕麻烦。
5.3 Swagger 打不开或报错
Swagger 打不开,先确认几件事:
- 当前环境是否
Development。很多项目只在开发环境启用 Swagger,生产环境自动关闭。 app.UseSwagger()和app.UseSwaggerUI()是否在app.UseAuthorization()之前或之后放在了正确位置。Swagger 的中间件不依赖认证顺序,但如果你开了认证而 Swagger 页面本身受限,需要额外放行。- DevExpress 的 WebApi Swashbuckle 扩展包是否安装完整。如果只装了
Swashbuckle.AspNetCore而没装DevExpress.ExpressApp.WebApi.Swashbuckle,运行时会因为缺少 XAF 的文档处理器而失败。
另外一个常见坑是 Swagger 页面能打开但显示“无法解析 API 定义”,此时多半是启动时抛了异常。查看应用日志,最常见的就是版本冲突或模块加载失败,跟 3.1 里提到的版本问题直接相关。
5.4 序列化循环引用和延迟加载爆炸
XAF 的业务对象往往存在多层关联关系,比如订单 -> 订单明细 -> 商品 -> 分类。如果你在 API 里直接返回IObjectSpace.GetObjects<Order>(),JSON 序列化器会尝试递归遍历所有导航属性,结果可能是三层之后爆出循环引用异常,或者把整个数据库都拖出来。
我第一次踩这个坑时,日志里报的是“Object graph contains circular reference”,当时还以为是 XAF 的 Bug,后来才发现是序列化层需要限制导航属性的遍历深度。
解决方案有两个层面。
第一层,在 Program.cs 中配置 JSON 选项:
builder.Services.AddMvc().AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; });这个配置能避免循环引用直接抛异常,但它只是“不报错”,该加载的数据还是会加载。
第二层,更推荐:API 返回 DTO(Data Transfer Object)。在查询时就只 Select 需要的字段,不触碰导航属性。比如查订单接口只返回订单号、客户名称、总金额,不把订单明细全量带出去。如果前端确实需要明细,就单独提供一个明细接口。这样既能控制返回体大小,也能避免延迟加载触发。
5.5 大批量修改事务与性能问题
如果你要批量导入或更新大量数据,比如一次性传入几百条记录,注意不要循环里提交。每CommitChanges()一次就是一次数据库往返,几百条循环就是几百次,性能直接崩掉。
正确做法是:在一个 ObjectSpace 里全部改完,最后提交一次。示例:
[HttpPost("bulk-update")] public IActionResult BulkUpdate([FromBody] List<ContactUpdateDto> items) { var application = _applicationFactory.CreateApplication(); using var objectSpace = application.CreateObjectSpace(typeof(Contact)); foreach (var item in items) { var contact = objectSpace.GetObjectByKey<Contact>(item.Id); if (contact != null) { contact.FullName = item.FullName; contact.Email = item.Email; } } objectSpace.CommitChanges(); return Ok(); }这样数据库只会产生一次事务提交,配合合理批次大小(比如每次调用最多传 1000 条),性能完全可控。如果仍然很慢,先排查数据库索引和网络延迟,别急着怀疑 XAF 本身。
6. 版本差异与调试经验
6.1 不同版本 API 名称有变化
DevExpress 每年的主版本都会调整一些 API 名称。我写这篇内容时,AddXafWebApi、UseXafWebApi、IXafApplicationFactory是当前主流的命名,但你在老版本项目(比如 v21.x)里可能看到的是其他扩展类。遇到编译报错不要慌,直接查版本对应的官方文档或升级指南。
同时注意,XAF 的 WebApi 模块在不同版本里对认证的默认行为也不完全一致。早期版本可能默认关闭标准认证,需要手动开启;新版本里开启方式更显式。每次升级后,务必回归一遍“未登录访问受保护接口”的用例,确认 401 逻辑符合预期。
6.2 调试建议:先只读后写入
我建议第一次集成时,不要一上来就写复杂的增删改接口。先做一个只读查询,跑通全链路:启动应用 → 打开 Swagger → 获取 Token → 调用接口 → 拿到数据。这条链路通了,后面加写入只是复制粘贴的问题。
获取 Token 的方式是:请求 Token 端点,传递用户名密码。XAF 标准认证的 Token 端点地址通常可以通过 Swagger UI 直接看到,那里会列出认证相关的接口。拿到 Token 后,在 Swagger UI 右上角点 Authorize,把 Token 填进去,之后就能调试受保护的接口了。
6.3 生产环境关闭 Swagger
Swagger 是开发调试利器,但在生产环境建议关闭。一方面是因为 Swagger 页面可能泄露接口结构和数据类型,增加攻击者探测攻击面的机会;另一方面,Swagger UI 本身偶尔会因为扫描整个 API 树造成不必要的性能开销。常规做法是在app.Environment.IsDevelopment()条件下调用UseSwagger和UseSwaggerUI。
7. 最后的一点心里话
做了这么多年 XAF 项目,我的体会是:集成 Web API 这件事,技术难点从来不在写 Controller,而在于你想清楚“哪些能力要复用、哪些边界要隔离”。把 API 直接挂进现有 XAF Blazor 应用,是成本最低、也最容易保持业务一致性的一条路。
如果你是从零开始,我建议先在现有项目里把只读接口跑通,感受一下 ObjectSpace 在 API 层的用法;等团队确实有了独立的 API 部署需求,再考虑把业务类库抽出去,那时候毕竟已经积累了接口层面的实践经验,迁移起来心里有数。