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
Swagger
Swashbuckle并不能直接支持OData,所以我们要做一些处理
services.AddSwaggerGen(c =>
{
//OData查QueryString
c.OperationFilter();
});
///
/// Swagger上OData参的数配置,这里把一些常用的参数说明提供出来
///
public class ODataParameterAttributeFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var enableQueryAttribute = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType().FirstOrDefault();
if (enableQueryAttribute != null)
{
var singleResultAttribute = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType().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"
}
});
}
}
}
}