.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"
                        }
                    });
                }
            }
        }
    }
    
posted @ 2021-01-17 09:08  shadowxs  阅读(747)  评论(0编辑  收藏  举报