当前位置: 首页 > news >正文

ABP VNext + OData:实现可查询的 REST API

🚀 ABP VNext + OData:实现可查询的 REST API


📚 目录

  • 🚀 ABP VNext + OData:实现可查询的 REST API
    • 一、版本说明 📦
    • 二、环境与依赖 ⚙️
    • 三、模块化注册 OData 与跨域 🌐
    • 四、实体 & DTO & MappingProfile 🗂️
    • 五、OData 控制器实现 🛠️
    • 六、全局 QuerySettings(可选简化方案) 🔄
    • 七、动态查询 & 导出示例 📈
    • 八、安全与性能最佳实践 🔒⚡
    • 九、配置示例:appsettings.json 📝
    • 十、端到端 Sequence 图 📊


一、版本说明 📦

组件版本
.NET SDK.NET 6+
ABP VNext6+
Microsoft.AspNetCore.OData8.0.8
AutoMapper.Extensions.ExpressionMapping12.0.x
Swashbuckle.AspNetCore.OData8.0.x

Tip:本文示例已在以上环境中验证通过,如有版本差异,请以官方文档为准。


二、环境与依赖 ⚙️

dotnet add package Microsoft.AspNetCore.OData --version 8.0.8
dotnet add package Microsoft.OData.ModelBuilder
dotnet add package AutoMapper.Extensions.ExpressionMapping
dotnet add package Swashbuckle.AspNetCore.OData

三、模块化注册 OData 与跨域 🌐

下面展示模块化注册 OData 中间件、启用 CORS、Swagger 扩展的完整流程:

Client Request
CORS Middleware
Routing & OData Middleware
ProductsController
GetQueryableAsync()
Database Query
Apply Filter & ProjectTo
Serialize JSON + @odata.count
Client Response
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.OData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using Swashbuckle.AspNetCore.OData;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Modularity;namespace YourProject.Web
{[DependsOn(typeof(AbpAspNetCoreMvcModule))]public class YourProjectWebModule : AbpModule{public override void ConfigureServices(ServiceConfigurationContext context){var configuration = context.Services.GetConfiguration();var odataCfg = configuration.GetSection("OData");var prefix = odataCfg["RoutePrefix"] ?? "api/odata";var maxTop = odataCfg.GetValue<int>("MaxTop", 100);var pageSize = odataCfg.GetValue<int>("PageSize", 50);var maxDepth = odataCfg.GetValue<int>("MaxExpansionDepth", 3);// 1️⃣ 跨域配置context.Services.AddCors(options =>{options.AddDefaultPolicy(builder =>builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());});// 2️⃣ 注册 OData + 属性路由context.Services.AddControllers().AddOData(opt => opt.Select().Filter().OrderBy().Expand().Count().SetMaxTop(maxTop)                     // 限制最大 $top.MaxExpansionDepth(maxDepth)          // 限制最大 $expand 深度.AddRouteComponents(prefix,                            // 路由前缀GetEdmModel(),services => services.EnableAttributeRouting = true));// 3️⃣ Swagger & OData 扩展context.Services.AddSwaggerGen(c =>{c.AddOData(prefix, GetEdmModel());});}public override void OnApplicationInitialization(ApplicationInitializationContext ctx){var app = ctx.GetApplicationBuilder();// 中间件执行顺序按 ASP.NET Core 最佳实践app.UseRouting();app.UseCors();app.UseAuthentication();app.UseAuthorization();app.UseSwagger();app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Your API V1"));app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}// 构建 EDM 模型public static IEdmModel GetEdmModel(){var builder = new ODataConventionModelBuilder();// --- ProductDto EDM 定义 ---var productType = builder.EntityType<ProductDto>();productType.HasKey(p => p.Id);productType.HasETag(p => p.LastModified);builder.EntitySet<ProductDto>("Products");// --- OrderDto EDM 定义 ---var orderType = builder.EntityType<OrderDto>();orderType.HasKey(o => o.Id);builder.EntitySet<OrderDto>("Orders");// --- 自定义 Function:MostExpensive(count) ---var fn = builder.Function("MostExpensive");fn.Parameter<int>("count");fn.ReturnsCollectionFromEntitySet<ProductDto>("Products");return builder.GetEdmModel();}}
}

四、实体 & DTO & MappingProfile 🗂️

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Domain.Entities.Auditing;namespace YourProject.Entities
{public class Product : AuditedAggregateRoot<Guid>{public string Name { get; set; }public decimal Price { get; set; }public bool IsDeleted { get; set; }[ConcurrencyCheck]  // 用于 ETag 并发控制public DateTimeOffset LastModified { get; set; }}
}namespace YourProject.Dtos
{public class ProductDto{public Guid Id { get; set; }public string Name { get; set; }public decimal Price { get; set; }public DateTimeOffset LastModified { get; set; }  // 用于 ETag}
}using AutoMapper;
namespace YourProject
{public class YourMappingProfile : Profile{public YourMappingProfile(){CreateMap<Product, ProductDto>();// LastModified 同名映射,无需额外 ForMember}}
}

五、OData 控制器实现 🛠️

using System;
using System.Linq;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Attributes;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Volo.Abp.Domain.Repositories;
using YourProject.Dtos;
using YourProject.Entities;namespace YourProject.Web.Controllers
{[ODataRoutePrefix("Products")][Authorize(AbpPermissions.Products.Default)]public class ProductsController : ODataController{private readonly IRepository<Product, Guid> _repo;private readonly IMapper _mapper;public ProductsController(IRepository<Product, Guid> repo, IMapper mapper){_repo   = repo;_mapper = mapper;}/// <summary>/// GET /api/odata/Products/// 支持 $filter, $orderby, $select, $skip/$top, $count/// </summary>[EnableQuery(PageSize = 50,MaxExpansionDepth = 3,// 排除 $apply, $searchAllowedQueryOptions =AllowedQueryOptions.All& ~AllowedQueryOptions.Apply& ~AllowedQueryOptions.Search)][ODataRoute]  public IActionResult Get(){var q = _repo.GetQueryableAsync().Result;  // 或使用 await/Task<IActionResult>q = q.Where(p => !p.IsDeleted);var projected = q.ProjectTo<ProductDto>(_mapper.ConfigurationProvider);return Ok(projected);}/// <summary>/// GET /api/odata/Products/MostExpensive(count=5)/// 自定义 Function:MostExpensive/// </summary>[EnableQuery(PageSize = 50, AllowedQueryOptions = AllowedQueryOptions.Select)][ODataRoute("MostExpensive(count={count})")]public IActionResult MostExpensive([FromODataUri] int count){var q = _repo.GetQueryableAsync().Result;var topN = q.Where(p => !p.IsDeleted).OrderByDescending(p => p.Price).Take(count).ProjectTo<ProductDto>(_mapper.ConfigurationProvider);return Ok(topN);}/// <summary>/// PATCH /api/odata/Products({id})/// 启用 ETag 并发检查/// </summary>[EnableQuery][AcceptVerbs("PATCH")][ODataRoute("({id})")]public IActionResult Patch([FromODataUri] Guid id, Delta<Product> delta){var entity = _repo.GetAsync(id).Result;delta.Patch(entity);  // If-Match 校验失败会抛 412_repo.UpdateAsync(entity).Wait();return Updated(entity);}}
}

💡Tips

  • 控制器继承自 ODataController,以获取 OData 原生的 Ok(), Updated() 等返回结果。
  • 若需异步完整,请将 .Result.Wait() 改为 async/await,并更改方法签名为 async Task<IActionResult>

六、全局 QuerySettings(可选简化方案) 🔄

context.Services.AddOData(opt => opt.Select().Filter().OrderBy().Expand().Count().QuerySettings(new DefaultQuerySettings {PageSize = 50,MaxExpansionDepth = 3,EnableFilter = true,EnableSelect = true,EnableOrderBy = true,EnableSkip = true,EnableTop = true}).AddRouteComponents("api/odata", GetEdmModel(), svc => svc.EnableAttributeRouting = true)
);

使用全局 QuerySettings 后,Controller 上可仅写 [EnableQuery]


七、动态查询 & 导出示例 📈

  • 筛选 & 排序

    GET /api/odata/Products?$filter=Price ge 100 and contains(Name,'Pro')&$orderby=Price desc
    
  • 分页 & 计数

    &$top=10&$skip=20&$count=true
    
  • 投影 & 展开

    &$select=Id,Name
    &$expand=Category($select=Name)
    

导出 CSV 示例

[HttpGet("export")]
public async Task<FileResult> ExportCsv([FromQuery] ODataQueryOptions<ProductDto> opts)
{var q = await _repo.GetQueryableAsync();var list = opts.ApplyTo(q).Cast<ProductDto>().ToList();var csv = CsvHelper.Write(list);return File(Encoding.UTF8.GetBytes(csv), "text/csv", "products.csv");
}

八、安全与性能最佳实践 🔒⚡

  1. 限流SetMaxTop(100)PageSize=50 防止一次性查询过大数据。
  2. 禁止高危选项:排除 $apply$search,避免聚合或全文搜索滥用。
  3. ETag 并发:结合 PATCH + If-Match,失败返回 412 Precondition Failed
  4. 缓存:对静态或少变资源开启 Redis 缓存,并结合 ETag 实现 304 Not Modified
  5. 索引优化:为常用筛选字段(如 PriceLastModified)建立数据库索引。
  6. 慢查询监控:记录 $filter / $orderby 参数与执行时长,设置多级告警阈值(200ms/500ms/1s)。

九、配置示例:appsettings.json 📝

{"Logging": { "LogLevel": { "Default": "Information" } },"AllowedHosts": "*","OData": {"RoutePrefix": "api/odata","MaxTop": 100,"PageSize": 50,"MaxExpansionDepth": 3}
}

十、端到端 Sequence 图 📊

ClientOData MiddlewareProductsControllerRepositoryGET /api/odata/Products?...Invoke Get()GetQueryableAsync()IQueryable<Product>Where + ProjectToIQueryable<ProductDto>JSON + @odata.countClientOData MiddlewareProductsControllerRepository

http://www.dtcms.com/a/298458.html

相关文章:

  • 服务端处于 TIME_WAIT 状态的 TCP 连接,收到相同四元组的 SYN 后会发生什么?详解
  • HCIP上HCIA复习静态综合实验
  • 移动端设备能部署的llm
  • 系统日志与用户信息绑定实现日志跟踪
  • 前端基础知识Vue系列 - 27(Vue项目中如何解决跨域)
  • 从 SQL Server 到 KingbaseES V9R4C12,一次“无痛”迁移与深度兼容体验实录
  • js基础概念-1
  • 牛客NC16660 [NOIP2004]FBI树(递归 + 二叉树后序遍历)
  • electron中IPC 渲染进程与主进程通信方法解析
  • 常用设计模式系列(十二)—享元模式
  • 如何在 FastAPI 中玩转 GraphQL 和 WebSocket 的实时数据推送魔法?
  • C++中使用Essentia实现STFT/ISTFT
  • git 连接GitHub仓库
  • 强化学习之策略熵坍塌优化-clip conv kv conv
  • 若依搭建详解
  • Android Paging 分页加载库详解与实践
  • 第七章 愿景11 琦琦复盘测试
  • Keepalived 深度技术解析与高可用实践指南
  • C++编程学习(第15天)
  • ServletRegistrationBean相关知识点
  • 用 Docker 一键部署 Flask + Redis 微服务
  • NX848NX854美光固态闪存NX861NX864
  • 截稿倒计时 TrustCom‘25大会即将召开
  • C++中AC、WA、RE、CE、TLE、MLE、PE、OLE的意思
  • 【ResNet50图像分类部署至RK3588】模型训练→转换RKNN→开发板部署
  • 安装本地python文件到site-packages
  • 专题:2025电商增长新势力洞察报告:区域裂变、平台垄断与银发平权|附260+报告PDF、原数据表汇总下载
  • Linux运维新人自用笔记(Rsync远程传输备份,服务端、邮箱和客户端配置、脚本)
  • 【c++思维题】洛谷 P1496 火烧赤壁
  • 【js(8) for...in和for...of】