【问题标题】:ASP.NET Core ways to handle custom response/output format in Web APIASP.NET Core 在 Web API 中处理自定义响应/输出格式的方法
【发布时间】:2018-10-28 14:06:14
【问题描述】:

我想创建自定义 JSON 格式,它将响应包装在数据中并返回 Content-Type

vnd.myapi+json

目前我已经创建了一个包装类,我在我的控制器中返回,但如果可以在后台处理它会更好:

public class ApiResult<TValue>
{
    [JsonProperty("data")]
    public TValue Value { get; set; }

    [JsonExtensionData]
    public Dictionary<string, object> Metadata { get; } = new Dictionary<string, object>();

    public ApiResult(TValue value)
    {
        Value = value;
    }
}

[HttpGet("{id}")]
public async Task<ActionResult<ApiResult<Bike>>> GetByIdAsync(int id)
{
    var bike = _dbContext.Bikes.AsNoTracking().SingleOrDefault(e => e.Id == id);
    if (bike == null)
    {
        return NotFound();
    }
    return new ApiResult(bike);
}

public static class ApiResultExtensions
{
    public static ApiResult<T> AddMetadata<T>(this ApiResult<T> result, string key, object value)
    {
        result.Metadata[key] = value;
        return result;
    }
}

我想返回如下响应:

{
    "data": { ... },
    "pagination": { ... },
    "someothermetadata": { ... }
}

但是必须以某种方式将分页添加到我的控制器操作中的元数据中,当然这里有一些关于内容协商的文章:https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-2.1 但我仍然想确保我走在正确的轨道上。

如果这将使用我的自定义格式化程序在后台处理,那么我将如何向它添加像分页这样的元数据,将其放在“数据”之外而不是其中?

当有一个自定义格式化程序时,我仍然希望有一些方法可以从我的控制器或通过某种机制向它添加元数据,以便格式可以扩展。

上述方法的一个优点或缺点是它适用于所有序列化程序 xml、json、yaml 等。通过使用自定义格式化程序,它可能仅适用于 json,我需要创建几个不同的格式化程序来支持所有我想要的格式。

【问题讨论】:

  • 我不确定您是否要说是否需要自定义数据格式,即不是 JSON,而是接近 JSON 的格式(在这种情况下为什么)。或者您担心数据本身的结构(这只是您序列化的模型)。或者,如果您担心 JSON 的格式 - 即换行符/制表符等(如果是这样,为什么)?
  • @JamesGaunt 我仍然想要 JSON,但我想将我的结果包装在“数据”中,这样​​我就可以在它旁边添加一些元数据,例如分页。这对于实现 HATEOAS 也很有用,我想在其中添加一些指向我的数据的链接以及可能的操作。例如jsonapi.org
  • 在这种情况下,我认为您的方法是正确的。这并不是真正的 mime 类型更改 - 它只是您的 API 的设计方式。因此,您需要向 JSON 序列化程序发送一个适当的模型——这正是您正在做的事情。您希望通过“隐藏”来获得什么?
  • 如果它只是您想要更改的 MIME 类型,您可以通过自己序列化 JSON 并返回具有您想要的任何 MIME 类型的内容结果来做到这一点。
  • 如果您只是不希望控制器中的代码(它显而易见且清晰) - 并且宁愿将其隐藏在某个地方 - 也许是一个 ResultFilter。不知道这是否是个好主意! docs.microsoft.com/en-us/aspnet/core/mvc/controllers/…

标签: c# asp.net-core asp.net-core-mvc asp.net-core-webapi


【解决方案1】:

好的,在花了一些时间使用 ASP.NET Core 之后,我基本上可以想到 4 种方法来解决这个问题。这个主题本身非常复杂和广泛,老实说,我不认为有灵丹妙药或最佳实践。

对于自定义 Content-Type(假设您要实现 application/hal+json),官方的方式可能是最优雅的方式是创建 custom output formatter。这样一来,您的操作就不会知道任何关于输出格式的信息,但由于依赖注入机制和作用域生命周期,您仍然可以控制控制器内部的格式化行为。


1. Custom output formatters

这是OData official C# librariesjson:api framework for ASP.Net Core 最常用的方式。可能是实现超媒体格式的最佳方式。

要从控制器控制您的自定义输出格式化程序,您必须创建自己的“上下文”以在控制器和自定义格式化程序之间传递数据,并将其添加到具有作用域生命周期的 DI 容器:

services.AddScoped&lt;ApiContext&gt;();

这样,每个请求将只有一个ApiContext 实例。您可以将它注入到您的控制器和输出格式化程序中,并在它们之间传递数据。

您还可以使用ActionContextAccessorHttpContextAccessor 并在您的自定义输出格式化程序中访问您的控制器和操作。要访问控制器,您必须将 ActionContextAccessor.ActionContext.ActionDescriptor 转换为 ControllerActionDescriptor。然后,您可以使用 IUrlHelper 和操作名称在输出格式化程序中生成链接,这样控制器就不会受到这种逻辑的影响。

IActionContextAccessor 是可选的,默认情况下不会添加到容器中,要在项目中使用它,您必须将其添加到 IoC 容器中。

services.AddSingleton&lt;IActionContextAccessor, ActionContextAccessor&gt;()

在自定义输出格式化程序中使用服务:

您不能在格式化程序类中进行构造函数依赖注入。例如,您无法通过向构造函数添加记录器参数来获取记录器。要访问服务,您必须使用传递给您的方法的上下文对象。

https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-2.0#read-write

Swashbuckle 支持

Swashbuckle 显然不会使用这种方法和使用过滤器的方法生成正确的响应示例。您可能需要创建您的自定义 document filter

示例:如何添加分页链接

通常分页、过滤是通过specification pattern 解决的,您通常会在[Get] 操作中为规范提供一些通用模型。然后,您可以在格式化程序中识别当前执行的操作是否通过其参数类型或其他内容返回元素列表:

var specificationParameter = actionContextAccessor.ActionContext.ActionDescriptor.Parameters.SingleOrDefault(p => p.ParameterType == typeof(ISpecification<>));
if (specificationParameter != null)
{
   // add pagination links or whatever
   var urlHelper = new UrlHelper(actionContextAccessor.ActionContext);
   var link = urlHelper.Action(new UrlActionContext()
   {
       Protocol = httpContext.Request.Scheme,
       Host = httpContext.Request.Host.ToUriComponent(),
       Values = yourspecification
   })
}

优点(或没有)

  • 您的操作没有定义格式,他们对格式或如何生成链接以及将链接放置在何处一无所知。他们只知道结果类型,而不知道描述结果的元数据。

  • 可重复使用,您可以轻松地将格式添加到其他项目中,而无需担心如何在您的操作中处理它。与链接、格式相关的所有内容都在后台处理。您的操作无需任何逻辑。

  • 序列化实现由你决定,你不必使用 Newtonsoft.JSON,你可以使用 Jil 例如。

缺点

  • 这种方法的一个缺点是它只适用于特定的 Content-Type。因此,为了支持 XML,我们需要创建另一个自定义输出格式化程序,其 Content-Type 为 vnd.myapi+xml 而不是 vnd.myapi+json

  • 我们不直接处理操作结果

  • 实现起来可能更复杂

2. Result filters

结果过滤器允许我们定义某种将在我们的操作返回之前执行的行为。我认为它是某种形式的后钩。我认为这不是包装我们回复的正确位置。

它们可以应用于每个操作或全局应用于所有操作。

就我个人而言,我不会将它用于这种事情,而是将其用作第 3 个选项的补充。

包装输出的示例结果过滤器:

public class ResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is ObjectResult objectResult)
        {
            objectResult.Value = new ApiResult { Data = objectResult.Value };
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

您可以将相同的逻辑放入IActionFilter,它应该也可以工作:

public class ActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is ObjectResult objectResult)
        {
            objectResult.Value = new ApiResult { Data = objectResult.Value };
        }
    }
}

这是包装响应的最简单方法,特别是如果您已经拥有带有控制器的现有项目。所以如果你在乎时间,就选这个吧。

3。在您的操作中明确格式化/包装您的结果

(我在问题中的做法)

这里也用到了:https://github.com/nbarbettini/BeautifulRestApi/tree/master/src 实现https://github.com/ionwg/ion-doc/blob/master/index.adoc 我个人认为这更适合自定义输出格式化程序。

这可能是最简单的方法,但它也将您的 API “密封”为特定格式。这种方法有优点,但也有一些缺点。例如,如果您想更改 API 的格式,则不能轻易做到,因为您的操作与特定的响应模型相结合,并且如果您的操作中有一些关于该模型的逻辑,例如,您为下一个和上一个重新添加分页链接。您实际上必须重写所有操作和格式化逻辑以支持该新格式。使用自定义输出格式化程序,您甚至可以根据 Content-Type 标头支持这两种格式。

优点:

  • 适用于所有 Content-Type,格式是 API 不可或缺的一部分。
  • Swashbuckle 开箱即用,使用ActionResult&lt;T&gt; (2.1+) 时,您还可以将[ProducesResponseType] 属性添加到您的操作中。

缺点:

  • 您无法使用Content-Type 标头控制格式。对于application/jsonapplication/xml,它始终保持不变。 (也许是优势?)
  • 您的操作负责返回格式正确的响应。类似:return new ApiResponse(obj); 或者您可以创建扩展方法并将其命名为 obj.ToResponse(),但您始终必须考虑正确的响应格式。
  • 理论上,像 vnd.myapi+json 这样的自定义 Content-Type 不会带来任何好处,并且仅仅为名称实现自定义输出格式化程序没有意义,因为格式化仍然是控制器操作的责任。

我认为这更像是正确处理输出格式的捷径。我认为遵循single responsibility principle 应该是输出格式化程序的工作,顾名思义它格式化输出。

4. Custom middleware

您可以做的最后一件事是自定义中间件,您可以从那里解析 IActionResultExecutor 并返回 IActionResult,就像您在 MVC 控制器中所做的那样。

https://github.com/aspnet/Mvc/issues/7238#issuecomment-357391426

如果您需要访问控制器信息,您还可以解析 IActionContextAccessor 以访问 MVC 的操作上下文并将 ActionDescriptor 转换为 ControllerActionDescriptor

文档说:

资源过滤器的工作方式类似于中间件,因为它们围绕管道中稍后出现的所有内容的执行。但过滤器与中间件的不同之处在于它们是 MVC 的一部分,这意味着它们可以访问 MVC 上下文和构造。

但这并不完全正确,因为您可以访问动作上下文,并且可以从中间件返回动作结果,这是 MVC 的一部分。


如果你有什么要补充的,分享你自己的经验和优缺点,欢迎评论。

【讨论】:

  • 很好的研究和很好的推理。真的很遗憾,这有这么少的赞成票
猜你喜欢
  • 2012-11-28
  • 1970-01-01
  • 1970-01-01
  • 2018-05-23
  • 2020-05-19
  • 2021-04-14
  • 1970-01-01
  • 2020-06-23
  • 2013-02-14
相关资源
最近更新 更多