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

发布于:2025-07-27 ⋅ 阅读:(18) ⋅ 点赞:(0)

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



一、版本说明 📦

组件 版本
.NET SDK .NET 6+
ABP VNext 6+
Microsoft.AspNetCore.OData 8.0.8
AutoMapper.Extensions.ExpressionMapping 12.0.x
Swashbuckle.AspNetCore.OData 8.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, $search
            AllowedQueryOptions =
                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 图 📊

Client OData Middleware ProductsController Repository GET /api/odata/Products?... Invoke Get() GetQueryableAsync() IQueryable<Product> Where + ProjectTo IQueryable<ProductDto> JSON + @odata.count Client OData Middleware ProductsController Repository


网站公告

今日签到

点亮在社区的每一天
去签到