.Net Core之OData
.Net Core之OData
OData可以说是轻量级的GraphQL,但又和GraphQL不同,配合Linq和EFCore,可以极大简化接口,提高开发效率。
但全套OData过重,坑也不少,所以我在项目里只使用了其Get部分的功能,同时重写了部分功能,配合EFCore实现高效开发
-
引入
services.AddOData(); services.AddODataQueryFilter();
-
启用
public static class ODataExtension { public static IEdmModel GetEdmModel(IServiceProvider serviceProvider) { var builder = new ODataConventionModelBuilder(serviceProvider); //默认情况OData返回的首字母大写(要想实现CamelCase,还必须要在AddMvc时配置Json: options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();) builder.EnableLowerCamelCase(); return builder.GetEdmModel(); } }
app.UseEndpoints(routeBuilder => { routeBuilder.Select().Filter().OrderBy().Expand().MaxTop(null).Count(); routeBuilder.MapODataRoute("api", "api", ODataExtension.GetEdmModel(app.ApplicationServices)); routeBuilder.EnableDependencyInjection(); });
-
接口
本身用OData很简单,但是配合EFCore使用过程中遇到了一些问题:
1.Expand,Expand对应EFCore的include,有不少坑,这里拦截Expand并自定义处理逻辑;
2.分页,自定义分页格式;
2.异步,支持异步;
[HttpGet] [MyEnableQuery(MaxExpansionDepth = 3)] //最多允许Expand三层 [AsyncQuery] public IActionResult Get() { return Ok(_repo.GetWithExpand(HttpContext?.Request?.Query)); } [HttpGet("{id}", Name = "GetById")] [MyEnableQuery(MaxExpansionDepth = 3)] [AsyncQuery(true)] [SingleResult] //用于Swagger public IActionResult GetById(int id) { return Ok(_repo.GetWithExpand(_repo.GetAll().Where(e => e.Id == id), HttpContext?.Request?.Query)); }
//拦截Query,移除原有Expand,自定义Expand逻辑 public class MyEnableQueryAttribute : EnableQueryAttribute { public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) { var newQueryOption = queryOptions.RemoveExpand(); return base.ApplyQuery(queryable, newQueryOption);; } }
public class AsyncQueryAttribute : ActionFilterAttribute { //是否返回单个值 private readonly bool _single; public AsyncQueryAttribute(bool single = false) { _single = single; } public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { var objectResult = context.Result as ObjectResult; if (objectResult?.Value is IQueryable queryable) { var result = queryable.Cast<object>(); var singleData = _single ? await result.FirstOrDefaultAsync() : null; var listData = _single ? null : await result.ToListAsync(); //分页: //如果配置了$count=true则OData会自动计算(TotalCount != null) //这个时候要返回count var count = context.HttpContext.Request.HttpContext.ODataFeature()?.TotalCount; if (count != null) { if (_single) { context.Result = new ObjectResult(new { count, singleData }); } else { context.Result = new ObjectResult(new { count, normalListData = listData }); } } else { context.Result = _single ? new ObjectResult(singleData) : new ObjectResult(listData); //单个返回值的情况下,如果不存在则返回404 if (_single && singleData == null) { context.Result = new NotFoundResult(); } } } await base.OnResultExecutionAsync(context, next); } }
-
Swagger
Swashbuckle并不能直接支持OData,所以我们要做一些处理
services.AddSwaggerGen(c => { //OData查QueryString c.OperationFilter<ODataParameterAttributeFilter>(); });
/// <summary> /// Swagger上OData参的数配置,这里把一些常用的参数说明提供出来 /// </summary> public class ODataParameterAttributeFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var enableQueryAttribute = context.MethodInfo.DeclaringType.GetCustomAttributes(true) .Union(context.MethodInfo.GetCustomAttributes(true)) .OfType<EnableQueryAttribute>().FirstOrDefault(); if (enableQueryAttribute != null) { var singleResultAttribute = context.MethodInfo.DeclaringType.GetCustomAttributes(true) .Union(context.MethodInfo.GetCustomAttributes(true)) .OfType<SingleResultAttribute>().FirstOrDefault(); var single = singleResultAttribute != null; //区分返回单条数据还是多条数据 if (single) { operation.Parameters.Add(new OpenApiParameter() { Name = "$filter", Description = "筛选条件(例:\n" + "1.等于(eq):Id eq 1;\n" + "2.不等于(ne):Id ne 1;\n" + "3.大于(gt):Id gt 0;\n" + "4.大于等于(ge):Id ge 0;\n" + "5.小于(lt):Id lt 0;\n" + "6.小于等于(le):Id le 0;\n" + "7.数组成员(in):Id in (1,2);\n" + "8.包含(contains):contains(Code,'code'));\n" + "9.层级筛选:User/Id eq 1;\n" + "10.多个条件:and/or;", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$expand", Description = "包含扩展对象(例:\n" + "1.单层:User;\n" + "2.嵌套:User($expand=User)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$select", Description = "包含字段(例:Id,Code)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); } else { operation.Parameters.Add(new OpenApiParameter { Name = "$filter", Description = "筛选条件(例:\n" + "1.等于(eq):Id eq 1;\n" + "2.不等于(ne):Id ne 1;\n" + "3.大于(gt):Id gt 0;\n" + "4.大于等于(ge):Id ge 0;\n" + "5.小于(lt):Id lt 0;\n" + "6.小于等于(le):Id le 0;\n" + "7.数组成员(in):Id in (1,2);\n" + "8.包含(contains):contains(Code,'code'));\n" + "9.层级筛选:User/Id eq 1;\n" + "10.多个条件:and/or;", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$orderby", Description = "排序(例:\n" + "1.Created asc;\n" + "2.Created desc)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$expand", Description = "包含扩展对象(例:\n" + "1.单层:User;\n" + "2.嵌套:User($expand=User))", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$select", Description = "包含字段(例:Id,Code)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "string" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$top", Description = "筛选数量(例:1)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "integer" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$skip", Description = "跳过数量(例:1)", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "integer" } }); operation.Parameters.Add(new OpenApiParameter { Name = "$count", Description = @"返回统计数量(例:true):$count = true时Model:{""count"": 1, ""data"": [{}]}", In = ParameterLocation.Query, Schema = new OpenApiSchema() { Type = "boolean" } }); } } } }