C#:如何在 API 控制器级别应用序列化以获取某些列

C#: How to Apply Serialization in API Controller Level to Obtain Certain Columns

我们目前有存储库层和应用服务层。

  1. Repo 使用 Entity Framework 从 SQL 服务器数据库获取数据。
  2. 应用服务层收集数据,并做更多的事情:发送电子邮件、解析平面文件,

回购层

public Task<Sales> GetBySalesId(int salesId)
{
    var salesData = _context.Sales
        .Include(c => c.Customer)
        .FirstOrDefaultAsync(c => c.SalesId == salesId);
    return salesData ;
}

服务层:

public async Task<SalesDto> GetSalesByIdAppService(int salesId)
{
    var salesData = await _salesRepository.GetBySalesId(salesId);
    var salesDto = _mapper.Map<SalesDto>(salesData);

    return salesDto;
}

这目前工作正常。但是明天,我的一位同事可能需要更多列,而我的特定应用程序部分不需要它们。

这里又添加了两个 Linq include: 但是,我不需要 Product 或 Employee。

新回购添加:

public Task<Sales> GetBySalesId(int salesId)
{
    var salesData = _context.Sales
        .Include(c => c.Customer)
        .Include(c => c.ProductType)
        .Include(c => c.Employee)
        .FirstOrDefaultAsync(c => c.SalesId == salesId);
    return salesData ;
}

背景一 建议创建另一个每个人都可以使用的中间域层。在 API DTO Level 中,每个人都可以拥有单独的 DTO,它只从 Domain 收集唯一需要的 class 成员。这基本上需要创建另一个层,其中 DTO 是新“域”层的子集。

*另一个建议是只对需要的列应用序列化。我一直听说这个,但是,如何做到这一点?是否可以在不添加其他层的情况下将应用程序序列化到 Controller API? Newtonsoft 是否有工具或 C# 中的任何语法?

API 控制器

public async Task<ActionResult<SalesDto>> GetSalesBySalesId(string salesId)
{
    var dto = await _service.GetBySalesId(salesId);
    return Ok(dto);
}

JSON 忽略可能不起作用,因为我们都共享相同的 DTO,并且忽略一个区域可能需要应用程序的其他部分。

使用 [JsonIgnore] 属性修饰 class 中的成员,这在响应中不是必需的。 JsonIgnore 可用于 System.Text.Json.Serialization 命名空间。

 public class SalesDto
{

    [JsonIgnore]
    public string Customer { get; set; }
    public string ProductType { get; set; }

    public string Employee { get; set; }
}

将模型的所有属性与数据绑定,当您将其发送到 UI 时,客户 属性 将无法响应。

我们应该从数据库中获取所有数据,并在我们的表示层中处理这些数据。 GraphQL,可能是这种情况的赢家,但需要探索

OData 作为 中间域层 可用于支持此类要求。它为调用者提供了对 Object Graphshape 的一些控制,即 returned。实现这一目标的代码过于复杂,无法作为单个 POST 包含,但是通常通过实施专门设计用于处理它们的架构来更好地解决这些类型的需求,而不是推出自己的 快速修复.

  • 您还可以查看 GraphQL,它允许在 returned 数据的 shape 上具有更大的灵活性,但是对于服务器端和客户端。

The problem with just using JsonIgnore is that this is a permanent definition on your DTO, which would make it hard to use the same DTO definitions for different applications that might require a different shape/view of the data.

A solution to that problem is then to create a tightly-coupled branch of DTOs for each application that inherits from the base but overrides the properties by decorating their properties with JsonIgnore attributes.

You want to avoid tightly coupled scenarios where the UI enforces too much structure on your data model, this can will make it harder to maintain your data model and can lead to many other anti-patterns down the track.


OData 允许一组 DTO 在后端和客户端具有一致的结构,同时允许客户端 reduce/omit/ignore它不知道或不想传输的字段。

The key here is that the Client now has (some) control over the graph, rather than the API needing to anticipate or tightly define the specific properties that each application MUST consume.

它有一个 rich standard convention based query language,这意味着许多现成的产品可以直接与您的 API

集成

一些优点供考虑:

  1. DTO的单一定义

    • 编写一次,定义所有应用程序都可以访问的数据模型的完整结构。
    • 客户可以通过query projections.
    • 指定他们需要的字段
  2. 通过API

    公开业务逻辑
    • 这是所有 APIs/服务层的一个特性,但是 OData 有一个支持和记录自定义业务的标准约定 Functions and Actions
  3. 在线路上的最小 DTO

    • OData 序列化程序可以配置为仅传输具有值的属性在线
    • 调用者可以指定要包含在查询结果中的字段
    • 当客户端未指定投影时,您可以配置默认属性和导航链接以发送资源请求。
  4. 补丁(增量)更新

    • OData 有一个简单的机制来支持对对象的部分更新,您只需通过网络发送任何对象的更改属性
      • 当然,这确实是前一点的一部分,但它是在基于标准 REST 的基础上使用 OData 的一个非常有力的论据 API
    • 您可以轻松地将业务规则注入查询验证管道(或执行)以防止更新特定字段,这可以包括基于角色或基于用户的安全上下文规则。
  5. .Net LINQ 支持
    虽然 API 是通过 URL 约定访问的,但这些约定可以通过 ODataLib or Simple OData Client.

    轻松映射到客户端的 .Net LINQ 查询
    • 并非所有可能的 LINQ 函数和查询都受支持,但足以完成工作。
    • 这意味着您可以轻松支持 C# 客户端应用程序。
  6. 可以避免版本控制
    这也成为 Con,但是可以轻松地将对数据模型的附加更改吸收到运行时中,而无需发布新的 version API.

    All of the above points contribute to allow Additive changes to be freely supported by down level clients without interruptions.

    • 在您想要重命名字段的情况下,也可以将一些旧字段映射到新定义,或者您可以为已删除的字段提供默认实现。
      • OData 模型是一种配置,它定义了 URL 请求如何映射或转换到 API 控制器上的端点,这意味着有一个层可以更改客户端请求映射到的方式

要注意的缺点

  1. 性能 - OData API 使用 HTTP 协议,因此与本地 DLL 调用相比存在固有的性能损失,windows 服务或 RPC,即使 API 和应用程序位于同一台机器上

    • 这种性能损失可以降到最低,通常是可以接受的总体成本
    • GraphQL 和大多数其他基于 REST/HTTP 的 API 架构将有类似的 JSON HTTP 性能问题。

    OData is still a Good fit for JSON over HTTP based remote API hosting scenarios, like javascript based clients.

  2. 更新相关对象支持不佳
    虽然 PATCH 适用于单个对象,但它不适用于嵌套对象图,要支持对嵌套对象的更新,您需要手动在客户端保留更改存储库并在每个对象上手动调用路径嵌套对象。

    • OData 协议确实提供了对多个查询进行批处理的规定,因此它们可以作为原子操作执行,但您仍然必须构建单独的更新。
    • 有一个 .Net based OData Client Generation Tool 可用于生成客户端存储库来为您管理。

    How often are you expecting your client to send back a rich collection of objects to update in a single hit?

    Is it a good idea to allow the client to do this, does an omitted field from the client mean we should set that property to null, does it mean we should delete that related data?

    Consider creating actions on the API to execute operations that affect multiple records to keep your clients thin and to consolidate logic so that each of your client applications does not have to re-implement the complex logic.

  3. 版本支持
    如果您希望允许对数据模型和 DTO 进行 破坏性 更改,则版本控制可能是一个长期问题。虽然标准 URL 约定支持对代码进行版本控制以实现它仍然很复杂

    Versioning can be hard to implement in any APIs, however the ability to set default projections for each DTO and with the client able to control their own specific projections means that the OData model is more resilient to additive-only changes, such as adding more tables, or fields.

    Additive changes can be implemented without interrupting client applications.

  4. 固定模式
    尽管客户端可以请求通过网络为 DTO 发送特定字段(包括导航属性),但客户端无法轻松请求数据以完全不同的结构返回。客户只能真正要求从结果中省略某些字段。

    • 支持 $apply URL 参数到 return 聚合操作的结果,但这不能完全控制结果的形状。

    GraphQL does address this exact issue. Moving the mapping from the API to the client side, giving the client a more control over the schema.

这个问题的解决方案非常简单,您可以为此使用半通用查询,而无需更改 DTO 中的任何内容。

首先让你的 repo 函数采用这样的通用包含:

public Task<Sales> GetBySalesId(string salesId, Func<IQueryable<Sales>, IIncludableQueryable<Sales, object>> include = null)
    {
        var query = _context.Sales.Where(x => x.Id == salesId);
        if (include != null)
            query = include(query);
        var salesData = query.FirstOrDefaultAsync();
        return salesData;
    }

这可以像这样在服务层中使用:

public async Task<Sales> GetById(string salesId)
    {
        var result = await _yourRepo.GetBySalesId(salesId, 
            include:  source => source
                .Include(a => a.SOMETHING)
                .Include(a => a.SOMETHING)
                .ThenInclude(a => a.SOMETHING));
        return result;
    }

现在要专门化查询结果,您可以根据您的令牌(如果您在 api 中使用授权)或者可以创建许多服务功能,根据您的条件调用它们在控制器中接收整数或其他东西。

public async Task<Sales> test(string salesId)
    {
        Func<IQueryable<Sales>, IIncludableQueryable<Sales, object>> include = null;
        if (UserRole == "YOU")
            include = a => a.Include(a => a.SOMETHING);
        else if (UserRole == "SomeoneElse")
            include = a => a.Include(a => a.SOMETHING).ThenInclude(a=>a.SOMETHINGELSE);

        var result = await _yourRepo.GetBySalesId(salesId, 
            include: include);
        return result;
    }

首先

你的逻辑很奇怪:你请求 DB return 所有列,然后只取几个需要的,效率低下。假设您有 20 列...

var salesData = await _salesRepository.GetBySalesId(salesId);
var salesDto = _mapper.Map<SalesDto>(salesData);

销售资料库应该能够包含客户和产品吗?

这可能是一个圣战主题。一般来说,如果您的架构不允许您从数据库切换到文件存储,从 ASP.NET MVC 切换到控制台应用程序,很可能它存在设计缺陷(并且它可能完全适合您公司当前的需求)

总结

您需要创建更多服务方法,构建结果,而不仅仅是按原样将数据从 Repo 传输到调用方。 对于您的情况,您需要您的服务覆盖更多场景

  • AppService.GetSalesById(销售编号)
  • AppService.GetSalesWithProductsById(销售额)
  • AppService.GetSalesById(salesId, includeProducts, includeCustomers)

我个人的偏好是用Commands更改多个参数,并使服务方法return Results.

如果您的同事要添加 2 列 - 将它们添加到现有结果中会更容易,如果同事正在编写新内容 - 最好引入新方法和结果

命令和结果

命令代表某些情况及其变体,服务看起来不错很干净。这种方法 在我过去 10 年的一个项目中经过时间测试 。我们已经切换了 3 次数据库,几个 ORM 和 2 个 UI。具体来说,我们使用 ICommandIResult 使其超级灵活。

API 控制器

public async Task<ActionResult<SalesDto>> GetSalesBySalesId(string salesId)
{
    UISalesTable dto = await (UISalesTable) _service.GetSales(new GetSalesForUICommand
    {
         SalesId = salesId,
         IncludeProductType = true,
         IncludeCustomer = false
    });

    return Ok(dto);
}

public async Task<ActionResult<MonthlySales>> GetSalesReport(string salesId)
{
    MonthlySales dto = await (MonthlySales) _service.GetSales(new GetMonthlySalesReportCommand
    {
         SalesId = salesId,
         // extra filters goes here
    });

    return Ok(dto);
}

服务层 有多少结果就有多少 DTO(不花钱)

public async Task<UISalesTable> GetSales(GetMonthlySalesReportCommand command)
{
    UISalesTable result = new UISalesTable();

    // good place to use Builder pattern
    result.SalesByMonthes = .....;

    TopProductsCalculator calc = new TopProductsCalculator();
    result.TopProducts = calc.Calculate(command.FromDate, command.ToDate);

    result.Etc = .....;

    return result;
}

这一切都取决于

很遗憾,没有食谱。它总是在质量和上市时间之间进行权衡。去年我更喜欢保持简单,甚至放弃了 repositories 的想法,现在我直接使用 DataContext,因为如果我要切换到 MongoDB,我将不得不再次编写每个存储库方法,并且这种事在一生中发生过几次。