当前位置: 首页 > 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.lryc.cn/news/600139.html

相关文章:

  • MyBatis-Plus极速开发指南
  • Springboot3.0 集成 RocketMQ5
  • 理解Spring中的IoC
  • 数字增加变化到目标数值动画,js实现
  • 2025年-ClickHouse 高性能实时分析数据库(大纲版)
  • cha的操作
  • 最新Amos 29下载及详细安装教程,附免激活中文版Amos安装包
  • Nature Communications:复杂光下多维视觉信息处理,利用时间演变的环境极化敏感神经突触器件
  • 基于Docker的GPU版本飞桨PaddleOCR部署深度指南(国内镜像)2025年7月底测试好用:从理论到实践的完整技术方案
  • JavaScript 中 let 在循环中的作用域机制解析
  • 【深度学习新浪潮】Claude code是什么样的一款产品?
  • swiper.js实现名录上下滚动
  • Python 条件分支与循环详解--python004
  • 【Agent】API Reference Manual(API 参考手册)
  • 【Spring AI详解】开启Java生态的智能应用开发新时代(附不同功能的Spring AI实战项目)
  • 手写A2C(FrozenLake环境)
  • 牛客刷题记录01
  • 【C++】二叉搜索数
  • 流式接口,断点续传解决方案及实现
  • QKV 为什么是三个矩阵?注意力为何要除以 √d?多头注意力到底有啥用?
  • 【PyTorch】图像多分类项目
  • jwt 在net9.0中做身份认证
  • qt框架,使用webEngine如何调试前端
  • 开发笔记 | 优化对话管理器脚本与对话语音的实现
  • 13.使用C连接mysql
  • Jenkins中出现pytest: error: unrecognized arguments: --alluredir=report错误解决办法
  • 栈----1.有效的括号
  • 机器学习 KNN 算法,鸢尾花案例
  • 从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】
  • C++ 多线程同步机制详解:互斥锁、条件变量与原子操作