使用具有复杂过滤器语法的外部 API
Consuming an External API that has a complex filter syntax
我需要List/Get/Update/Create/Destroy(即执行 CRUD 活动)来自外部 REST API 的数据。
此 API 具有自定义过滤器语法,如下所示:
{{BaseUrl}}/V1.0/<Entity>/query?search={"filter":[{"op":"eq","field":"id","value":"68275"}]}
这个过滤器语法非常灵活,基本上可以让你做 fieldA == x
AND/OR
fieldB != y
查询。
id <= 1000 && Title == "Some title"
{
"filter": [
{
"op": "le",
"field": "id",
"value": 1000
},
{
"op": "eq",
"field": "Title",
"value": "Some title"
}
]
}
firstname == "john" || lastname != "Jones"
{
"filter": [
{
"op": "or",
"items": [
{
"op": "eq",
"field": "firstname",
"value": "John"
},
{
"op": "ne",
"field": "lastname",
"value": "Jones"
}
]
}
]
}
如果你很好奇,那就是 Autotask API:https://ww3.autotask.net/help/DeveloperHelp/Content/APIs/REST/General_Topics/REST_Swagger_UI.htm
目前,我有一些 类 可以转换为第一个示例查询 id <= 1000 && Title == "Some title"
。
public interface IAutotaskFilter
{
string Field { get; }
string Value { get; }
string ComparisonOperator { get; }
}
public interface IAutotaskQuery
{
void AddFilter(IAutotaskFilter autotaskFilter);
void AddFilters(IList<IAutotaskFilter> filters);
void RemoveFilter(IAutotaskFilter autotaskFilter);
}
问题是这违反了我的干净架构。
Clean Architecture Example from Microsoft
如果我使用这些 类(通过上述接口),那么我的应用层将取决于我的基础设施层的实现细节。换句话说,我的业务逻辑将知道如何为这个特定的外部构造查询 API.
据我所知,我有几个选择:
实施自定义 LINQ 提供程序,将 linq 查询转换为此语法。据我所知,这非常困难。我没有找到任何关于此的最新信息。任何尝试过的图书馆至少有 7 年没有被碰过。如果这是可能的,我很乐意这样做。拥有流畅的查询语法对我来说非常有吸引力。
收起并按原样使用。
在本地缓存 API 实体并使用它们的 webhooks 'updated'/'created'/'destroyed' 事件进行缓存失效。这是大量的处理、数据传输和复杂性,所以可能不值得。
我希望有更好的选择,但我还没有找到。请告诉我。
如果有帮助,Autotask API 提供了一个 OpenAPI v1 文档。
我已经了解了一些关于如何不在您当前的域中引入新模型的问题。并提出了一个解决方案,它添加了新的依赖项,但不需要新的领域模型。这是一个可以使用的可能解决方案:
一个抽象,它将根据业务需求为您提供域实体:
public interface IEntityProvider
{
Task<Entity> GetEntityAsync(string structuredData);
}
"structuredData" 将是一个字符串,它将帮助您构建过滤器以发出第 3 方请求。要获得它,您将有一个帮手,他会为您完成这项工作:
public interface IRequestEncoder
{
string GetStructuredData(/* arguments */);
}
public class RequestEncoder : IRequestEncoder
{
public static string GetStructuredData(/* arguments */)
{
// Structuring the data in a way, that can be later mapped 1 to 1 to filters.
}
}
您还需要一个解码器,它将被注入到“EntityProvider”的实现中:
public interface IRequestDecoder
{
IEnumerable<IAutotaskFilter> GetFilter(string structuredData);
}
最后,带有过滤器逻辑的封装模块可能如下所示:(它将与您的过滤器模型一起位于域之外)
public class EntityProvider : IEntityProvider
{
private readonly IRequestDecoder _requestDecoder;
public EntityProvider(IRequestDecoder requestDecoder)
{
_requestDecoder = requestDecoder;
}
public async Task<Land> GetEntityAsync(string structuredData)
{
// Analyze the structure data, make sure it's decodable
// Convert structureData to filters using corresponding component (IRequestDecoder)
// Request the external service, passing necessary parameters
// Return domain model
}
}
P.S。当然 interfaces/contracts 可能并且可能确实与您的业务需求实际不同,您也可能想出更好的组件名称,但我希望这个想法能为您提供一些价值。
查看 system.linq.expressions
命名空间中的 Expression
class 和 ExpressionVisitor
https://docs.microsoft.com/en-us/dotnet/api/system.linq.expressions?view=net-5.0
它是LINQ 的精髓,但没有SQL 数据库部分。它允许您在 C# 中编写表达式并将它们转换为任何格式。请参阅下面的代码示例。
通过将像 (bo) => bo.MyID == 1 || bo.MyID == 3
这样的 lambda 表达式分配给和表达式类型,原始表达式在代码中变得可用。
这是 C# 编译器的一项功能,LINQ 使用它在查询被编译为字节代码之前捕获它。
更改下面代码的 TextWriter 部分,以 API 要求的格式写入字符串。
class BObject
{
public int MyID;
}
class MyExpressionVisitor : ExpressionVisitor
{
private TextWriter writer;
public MyExpressionVisitor(TextWriter writer)
{
this.writer = writer;
}
protected override Expression VisitBinary(BinaryExpression node)
{
writer.WriteLine("Binary node " + node.NodeType);
return base.VisitBinary(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
writer.WriteLine("Constant node " + node.NodeType + " Value " + node.Value);
return base.VisitConstant(node);
}
protected override Expression VisitMember(MemberExpression node)
{
writer.WriteLine("Constant node " + node.NodeType + " Member " + node.Member);
return base.VisitMember(node);
}
}
class Program
{
static void Main(string[] args)
{
Expression<Func<BObject, bool>> query = (bo) => bo.MyID == 1 || bo.MyID == 3;
var visitor = new MyExpressionVisitor(Console.Out);
visitor.Visit(query.Body);
Console.ReadKey();
}
}
输出:
Binary node OrElse
Binary node Equal
Constant node MemberAccess Member Int32 MyID
Constant node Constant Value 1
Binary node Equal
Constant node MemberAccess Member Int32 MyID
Constant node Constant Value 3
我需要List/Get/Update/Create/Destroy(即执行 CRUD 活动)来自外部 REST API 的数据。
此 API 具有自定义过滤器语法,如下所示:
{{BaseUrl}}/V1.0/<Entity>/query?search={"filter":[{"op":"eq","field":"id","value":"68275"}]}
这个过滤器语法非常灵活,基本上可以让你做 fieldA == x
AND/OR
fieldB != y
查询。
id <= 1000 && Title == "Some title"
{
"filter": [
{
"op": "le",
"field": "id",
"value": 1000
},
{
"op": "eq",
"field": "Title",
"value": "Some title"
}
]
}
firstname == "john" || lastname != "Jones"
{
"filter": [
{
"op": "or",
"items": [
{
"op": "eq",
"field": "firstname",
"value": "John"
},
{
"op": "ne",
"field": "lastname",
"value": "Jones"
}
]
}
]
}
如果你很好奇,那就是 Autotask API:https://ww3.autotask.net/help/DeveloperHelp/Content/APIs/REST/General_Topics/REST_Swagger_UI.htm
目前,我有一些 类 可以转换为第一个示例查询 id <= 1000 && Title == "Some title"
。
public interface IAutotaskFilter
{
string Field { get; }
string Value { get; }
string ComparisonOperator { get; }
}
public interface IAutotaskQuery
{
void AddFilter(IAutotaskFilter autotaskFilter);
void AddFilters(IList<IAutotaskFilter> filters);
void RemoveFilter(IAutotaskFilter autotaskFilter);
}
问题是这违反了我的干净架构。
Clean Architecture Example from Microsoft
如果我使用这些 类(通过上述接口),那么我的应用层将取决于我的基础设施层的实现细节。换句话说,我的业务逻辑将知道如何为这个特定的外部构造查询 API.
据我所知,我有几个选择:
实施自定义 LINQ 提供程序,将 linq 查询转换为此语法。据我所知,这非常困难。我没有找到任何关于此的最新信息。任何尝试过的图书馆至少有 7 年没有被碰过。如果这是可能的,我很乐意这样做。拥有流畅的查询语法对我来说非常有吸引力。
收起并按原样使用。
在本地缓存 API 实体并使用它们的 webhooks 'updated'/'created'/'destroyed' 事件进行缓存失效。这是大量的处理、数据传输和复杂性,所以可能不值得。
我希望有更好的选择,但我还没有找到。请告诉我。
如果有帮助,Autotask API 提供了一个 OpenAPI v1 文档。
我已经了解了一些关于如何不在您当前的域中引入新模型的问题。并提出了一个解决方案,它添加了新的依赖项,但不需要新的领域模型。这是一个可以使用的可能解决方案:
一个抽象,它将根据业务需求为您提供域实体:
public interface IEntityProvider
{
Task<Entity> GetEntityAsync(string structuredData);
}
"structuredData" 将是一个字符串,它将帮助您构建过滤器以发出第 3 方请求。要获得它,您将有一个帮手,他会为您完成这项工作:
public interface IRequestEncoder
{
string GetStructuredData(/* arguments */);
}
public class RequestEncoder : IRequestEncoder
{
public static string GetStructuredData(/* arguments */)
{
// Structuring the data in a way, that can be later mapped 1 to 1 to filters.
}
}
您还需要一个解码器,它将被注入到“EntityProvider”的实现中:
public interface IRequestDecoder
{
IEnumerable<IAutotaskFilter> GetFilter(string structuredData);
}
最后,带有过滤器逻辑的封装模块可能如下所示:(它将与您的过滤器模型一起位于域之外)
public class EntityProvider : IEntityProvider
{
private readonly IRequestDecoder _requestDecoder;
public EntityProvider(IRequestDecoder requestDecoder)
{
_requestDecoder = requestDecoder;
}
public async Task<Land> GetEntityAsync(string structuredData)
{
// Analyze the structure data, make sure it's decodable
// Convert structureData to filters using corresponding component (IRequestDecoder)
// Request the external service, passing necessary parameters
// Return domain model
}
}
P.S。当然 interfaces/contracts 可能并且可能确实与您的业务需求实际不同,您也可能想出更好的组件名称,但我希望这个想法能为您提供一些价值。
查看 system.linq.expressions
命名空间中的 Expression
class 和 ExpressionVisitor
https://docs.microsoft.com/en-us/dotnet/api/system.linq.expressions?view=net-5.0
它是LINQ 的精髓,但没有SQL 数据库部分。它允许您在 C# 中编写表达式并将它们转换为任何格式。请参阅下面的代码示例。
通过将像 (bo) => bo.MyID == 1 || bo.MyID == 3
这样的 lambda 表达式分配给和表达式类型,原始表达式在代码中变得可用。
这是 C# 编译器的一项功能,LINQ 使用它在查询被编译为字节代码之前捕获它。
更改下面代码的 TextWriter 部分,以 API 要求的格式写入字符串。
class BObject
{
public int MyID;
}
class MyExpressionVisitor : ExpressionVisitor
{
private TextWriter writer;
public MyExpressionVisitor(TextWriter writer)
{
this.writer = writer;
}
protected override Expression VisitBinary(BinaryExpression node)
{
writer.WriteLine("Binary node " + node.NodeType);
return base.VisitBinary(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
writer.WriteLine("Constant node " + node.NodeType + " Value " + node.Value);
return base.VisitConstant(node);
}
protected override Expression VisitMember(MemberExpression node)
{
writer.WriteLine("Constant node " + node.NodeType + " Member " + node.Member);
return base.VisitMember(node);
}
}
class Program
{
static void Main(string[] args)
{
Expression<Func<BObject, bool>> query = (bo) => bo.MyID == 1 || bo.MyID == 3;
var visitor = new MyExpressionVisitor(Console.Out);
visitor.Visit(query.Body);
Console.ReadKey();
}
}
输出:
Binary node OrElse
Binary node Equal
Constant node MemberAccess Member Int32 MyID
Constant node Constant Value 1
Binary node Equal
Constant node MemberAccess Member Int32 MyID
Constant node Constant Value 3