11. 【Blazor全栈开发实战指南】--构建ASP.NET Core Web API作为后端

发布时间:2026/7/3 3:31:39

11. 【Blazor全栈开发实战指南】--构建ASP.NET Core Web API作为后端 一、RESTful API设计原则与.NET 10 Minimal API在Blazor全栈应用中前端Blazor组件负责呈现UI和处理用户交互而数据的存储、业务逻辑和安全验证通常交由ASP.NET Core Web API来承载。这种前后端分离的架构中两端通过标准的HTTP协议和JSON数据格式通信彼此解耦可以独立部署和扩展。好的API设计遵循REST表述性状态传递原则其核心是用URL描述资源用HTTP动词描述操作。以产品资源为例GET /api/products— 获取所有产品集合资源GET /api/products/{id}— 获取指定产品单个资源POST /api/products— 创建新产品请求体携带数据PUT /api/products/{id}— 全量更新指定产品PATCH /api/products/{id}— 部分更新指定产品DELETE /api/products/{id}— 删除指定产品.NET 10的Minimal API风格极大地简化了API端点的定义无需Controller类直接在Program.cs或通过扩展方法注册路由非常适合构建精简的微服务或中小型应用// Program.csAPI项目varbuilderWebApplication.CreateBuilder(args);// 注册数据库上下文使用Entity Framework Core详见第六章builder.Services.AddDbContextAppDbContext(optionsoptions.UseSqlite(Data Sourceapp.db));// 注册Swagger文档生成开发调试利器builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();// 注册JSON序列化选项builder.Services.ConfigureHttpJsonOptions(options{// 忽略null值字段减少响应体大小options.SerializerOptions.DefaultIgnoreConditionSystem.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;// 属性名使用驼峰命名camelCase符合JS约定options.SerializerOptions.PropertyNamingPolicySystem.Text.Json.JsonNamingPolicy.CamelCase;});varappbuilder.Build();if(app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}// 将所有产品相关端点组织到 /api/products 路由组varproductsApiapp.MapGroup(/api/products).WithTags(Products);// GET /api/products?page1pageSize10productsApi.MapGet(/,async(AppDbContextdb,intpage1,intpageSize10){vartotalawaitdb.Products.CountAsync();varitemsawaitdb.Products.OrderBy(pp.Id).Skip((page-1)*pageSize).Take(pageSize).Select(pnewProductDto(p.Id,p.Name,p.Price,p.Stock)).ToListAsync();// 将分页元数据放入响应头保持响应体干净returnResults.Ok(newPagedResultProductDto(items,total,page,pageSize));});// GET /api/products/{id}productsApi.MapGet(/{id:int},async(intid,AppDbContextdb){varproductawaitdb.Products.FindAsync(id);// 资源不存在时返回404 Not Foundreturnproductisnull?Results.NotFound(new{message$产品{id}不存在}):Results.Ok(newProductDto(product.Id,product.Name,product.Price,product.Stock));});// POST /api/productsproductsApi.MapPost(/,async(CreateProductRequestrequest,AppDbContextdb){varproductnewProduct{Namerequest.Name,Pricerequest.Price,Stockrequest.Stock};db.Products.Add(product);awaitdb.SaveChangesAsync();// 创建成功返回201 Created并在Location Header中携带新资源的URLreturnResults.CreatedAtRoute(GetProduct,new{idproduct.Id},newProductDto(product.Id,product.Name,product.Price,product.Stock));}).WithName(GetProduct);// PUT /api/products/{id}productsApi.MapPut(/{id:int},async(intid,UpdateProductRequestrequest,AppDbContextdb){varproductawaitdb.Products.FindAsync(id);if(productisnull)returnResults.NotFound();product.Namerequest.Name;product.Pricerequest.Price;product.Stockrequest.Stock;awaitdb.SaveChangesAsync();returnResults.NoContent();// 204 No Content成功但无内容返回});// DELETE /api/products/{id}productsApi.MapDelete(/{id:int},async(intid,AppDbContextdb){varproductawaitdb.Products.FindAsync(id);if(productisnull)returnResults.NotFound();db.Products.Remove(product);awaitdb.SaveChangesAsync();returnResults.NoContent();});app.Run();注意这里使用了DTO数据传输对象模式API返回的ProductDto与数据库实体Product是分开的两种类型。这种分离是良好设计的关键实践——它防止了数据库内部结构意外暴露给API消费者并让API契约的演进与数据库结构的变化互相独立。二、Blazor前端的CORS配置与HttpClient设置当Blazor WebAssembly应用通常运行在https://localhost:5001向API服务器通常在另一个端口如https://localhost:7000发起请求时浏览器的**跨域资源共享CORS**安全策略会阻止这个请求除非API服务器显式声明允许来自前端域名的跨域请求。在API项目的Program.cs中配置CORS策略// CORS配置API服务器端builder.Services.AddCors(options{options.AddPolicy(BlazorClientPolicy,policy{policy// 精确指定允许的前端域名不要在生产环境使用 AllowAnyOrigin.WithOrigins(https://localhost:5001,https://myapp.com)// 允许所有HTTP方法GET、POST、PUT等.AllowAnyMethod()// 允许携带Authorization等自定义请求头.AllowAnyHeader()// 如果需要发送Cookie凭据需要 AllowCredentials()// 注意AllowCredentials 不能与 AllowAnyOrigin 同时使用.AllowCredentials();});});// 在中间件管道中启用CORS策略必须在 UseRouting 之前app.UseCors(BlazorClientPolicy);在Blazor WebAssembly前端项目的Program.cs中注册一个配置好BaseAddress的HttpClient// Blazor WebAssembly 前端 Program.csbuilder.Services.AddScoped(_newHttpClient{BaseAddressnewUri(https://localhost:7000/)});通过将HttpClient注册到DI容器任何组件或服务都可以通过inject HttpClient Http获取它无需关心基础URL的配置。三、HttpClient与类型化客户端在组件中直接注入HttpClient是最直接的方式但对于复杂应用将API调用封装到专门的**类型化客户端Typed Client**中是更佳的实践它实现了关注点分离使API逻辑集中管理// ApiClients/ProductApiClient.cspublicclassProductApiClient{privatereadonlyHttpClient_http;// 类型化客户端通过构造函数接收HttpClientIHttpClientFactory负责创建和生命周期管理publicProductApiClient(HttpClienthttp){_httphttp;}publicasyncTaskPagedResultProductDto?GetProductsAsync(intpage1,intpageSize10){// GetFromJsonAsync 结合了发请求和反序列化两个步骤是 System.Net.Http.Json 扩展方法returnawait_http.GetFromJsonAsyncPagedResultProductDto($api/products?page{page}pageSize{pageSize});}publicasyncTaskProductDto?GetProductAsync(intid){try{returnawait_http.GetFromJsonAsyncProductDto($api/products/{id});}catch(HttpRequestExceptionex)when(ex.StatusCodeSystem.Net.HttpStatusCode.NotFound){returnnull;// 将404转换为null由调用方决定如何处理}}publicasyncTaskProductDto?CreateProductAsync(CreateProductRequestrequest){// PostAsJsonAsync 自动序列化请求体为JSON并设置 Content-Type: application/jsonvarresponseawait_http.PostAsJsonAsync(api/products,request);response.EnsureSuccessStatusCode();// 非2xx状态码时抛出异常returnawaitresponse.Content.ReadFromJsonAsyncProductDto();}publicasyncTaskUpdateProductAsync(intid,UpdateProductRequestrequest){varresponseawait_http.PutAsJsonAsync($api/products/{id},request);response.EnsureSuccessStatusCode();}publicasyncTaskDeleteProductAsync(intid){varresponseawait_http.DeleteAsync($api/products/{id});response.EnsureSuccessStatusCode();}}在Program.cs中注册类型化客户端同时指定基础地址// 使用 IHttpClientFactory 管理 HttpClient 的生命周期避免 Socket 耗尽问题builder.Services.AddHttpClientProductApiClient(client{client.BaseAddressnewUri(https://localhost:7000/);client.TimeoutTimeSpan.FromSeconds(30);});在组件中注入类型化客户端并使用page/productsinject ProductApiClient ProductApih1产品管理/h1if(pagedResultisnull){p加载中.../p}else{tableclasstabletheadtrth名称/thth价格/thth库存/thth操作/th/tr/theadtbodyforeach(varproductinpagedResult.Items){trtdproduct.Name/tdtd¥product.Price.ToString(F2)/tdtdproduct.Stock/tdtdbuttononclick() DeleteProduct(product.Id)classbtn btn-sm btn-danger删除/button/td/tr}/tbody/tablePaginationCurrentPagecurrentPageTotalPagespagedResult.TotalPagesOnPageChangedLoadProducts/}code{privatePagedResultProductDto?pagedResult;privateintcurrentPage1;protectedoverrideasyncTaskOnInitializedAsync(){awaitLoadProducts(1);}privateasyncTaskLoadProducts(intpage){currentPagepage;pagedResultawaitProductApi.GetProductsAsync(page);}privateasyncTaskDeleteProduct(intid){awaitProductApi.DeleteProductAsync(id);awaitLoadProducts(currentPage);// 删除后刷新当前页}}四、JSON序列化配置与DTO设计.NET 10默认使用System.Text.Json进行JSON处理它比Newtonsoft.Json有更好的性能但默认行为与JavaScript的JSON约定有所差异需要统一配置。最佳实践是使用C#record类型定义DTO它是不可变的值类型具有自动生成的构造函数、相等性比较和ToString方法非常适合作为API的数据传输载体// 使用 record 定义不可变 DTOpublicrecordProductDto(intId,stringName,decimalPrice,intStock);// 创建请求DTO包含数据验证publicrecordCreateProductRequest{[Required(ErrorMessage产品名称不能为空)][StringLength(100,ErrorMessage名称最长100个字符)]publicrequiredstringName{get;init;}[Range(0.01,999999.99,ErrorMessage价格必须在0.01到999999.99之间)]publicrequireddecimalPrice{get;init;}[Range(0,int.MaxValue,ErrorMessage库存不能为负数)]publicintStock{get;init;}0;}// 通用分页结果包装类publicrecordPagedResultT(IReadOnlyListTItems,intTotalCount,intCurrentPage,intTotalPages);对于枚举类型默认情况下System.Text.Json会将枚举序列化为数字在API的JSON响应中数字枚举可读性很差。通过在Program.cs中全局配置JsonStringEnumConverter可以让枚举以字符串形式传输builder.Services.ConfigureHttpJsonOptions(options{options.SerializerOptions.Converters.Add(newSystem.Text.Json.Serialization.JsonStringEnumConverter());});五、总结本章从RESTful设计原则出发使用.NET 10 Minimal API构建了一套完整的产品资源CRUD接口解决了Blazor WebAssembly跨域访问的CORS配置问题介绍了类型化客户端模式封装HttpClient使API调用代码集中、可复用最后探讨了record类型DTO和System.Text.Json的最佳实践配置。然而一个面向真实用户的应用不能对任何人开放所有接口——删除、修改等敏感操作必须要求用户身份验证且不同角色的用户只能访问相应权限范围内的资源。下一章我们将深入认证与授权机制学习JWT令牌的完整流程以及Blazor客户端如何安全地持有和使用访问令牌。

相关新闻