ASP.NET Core在Web API中处理自定义响应/输出格式的方法

时间:2018-05-18 07:33:30

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

我想创建自定义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,我需要创建几个不同的格式化程序来支持所有我想要的格式。

1 个答案:

答案 0 :(得分:8)

好的,在花了大量时间使用ASP.NET Core后,基本上有4种方法可以解决这个问题。这个主题本身非常复杂和广泛,想想并且说实话,我不认为这是一个银弹或最佳实践。

对于自定义内容类型(让我们说你想实现application/hal+json),官方方式,也许最优雅的方式是创建custom output formatter。这样,您的操作就不会对输出格式有任何了解,但由于依赖注入机制和作用域生存期,您仍然可以控制控制器内的格式化行为。

1. Custom output formatters

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

要从控制器控制自定义输出格式化程序,您必须创建自己的&#34; context&#34;在控制器和自定义格式化程序之间传递数据,并将其添加到带有作用域生存期的DI容器中:

services.AddScoped<ApiContext>();

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

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

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

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>()

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

  

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

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,我们需要创建另一个自定义输出格式化程序,其内容类型为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我认为这更适合自定义输出格式化程序。

这可能是最简单的方法,但它也是&#34;密封&#34;您对该特定格式的API。这种方法有优点,但也存在一些缺点。例如,如果您想要更改API的格式,则无法轻松完成,因为您的操作与特定的响应模型相结合,并且如果您的操作中有该模型的某些逻辑,例如,你为next和prev添加了分页链接。您几乎必须重写所有操作和格式化逻辑以支持该新格式。使用自定义输出格式化程序,您甚至可以支持这两种格式,具体取决于Content-Type标题。

<强>优点:

  • 适用于所有内容类型,格式是API的组成部分。
  • Swashbuckle开箱即用,使用ActionResult<T>(2.1+)时,您还可以为您的操作添加[ProducesResponseType]属性。

<强>缺点:

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

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

4. Custom middleware

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

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

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

文档说:

  

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

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

如果您有任何要添加的内容,请分享您自己的经验和优缺点,随时发表评论。