使用具有复杂过滤器语法的外部 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.

据我所知,我有几个选择:

我希望有更好的选择,但我还没有找到。请告诉我。

如果有帮助,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