简体   繁体   English

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

[英]ASP.NET Core ways to handle custom response/output format in Web API

I'd like to create custom JSON format, that would wrap the response in data and would return Content-Type like我想创建自定义 JSON 格式,它将响应包装在数据中并返回 Content-Type 之类的

vnd.myapi+json vnd.myapi+json

Currently I have created like a wrapper classes that I return in my controllers but it would be nicer if that could be handled under the hood:目前我已经创建了一个像我在控制器中返回的包装类,但如果可以在后台处理它会更好:

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;
    }
}

I'd like to return response like:我想返回如下响应:

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

But the pagination would have to be added somehow to the metadata in my controller's action, of course there's some article about content negotiation here: https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-2.1 but still I'd like to be sure I'm on the right track.但是分页必须以某种方式添加到我的控制器操作中的元数据中,当然这里有一些关于内容协商的文章: https : //docs.microsoft.com/en-us/aspnet/core/web-api/advanced /formatting?view=aspnetcore-2.1但我仍然想确定我在正确的轨道上。

If that would be handled under the hood with my custom formatter then how would I add metadata like a pagination to it, to be aside of "data" and not inside of it?如果这将使用我的自定义格式化程序在引擎盖下处理,那么我将如何向其添加分页之类的元数据,而不是在“数据”之外而不是在其中?

When having a custom formatter I'd like to still have some way to add metadata to it from my controllers or by some mechanism so the format could be extensible.当有一个自定义格式化程序时,我仍然希望有一些方法可以从我的控制器或通过某种机制向它添加元数据,以便格式可以扩展。

One advantage or disadvantage with the approach above is that it works with all serializers xml, json, yaml etc. By having custom formatter it would probably work only for json, and I will need to create few different formatters to support all the formats that I want.上述方法的一个优点或缺点是它适用于所有序列化程序 xml、json、yaml 等。通过使用自定义格式化程序,它可能仅适用于 json,我将需要创建几个不同的格式化程序来支持我使用的所有格式想。

Okay, after spending some good amount of time with ASP.NET Core there are basically 4 ways I can think of to solve this.好的,在 ASP.NET Core 上花了很多时间之后,我基本上可以想到 4 种方法来解决这个问题。 The topic itself is quite complex and broad to think of and honestly, I don't think there's a silver bullet or the best practice for this.这个话题本身是相当复杂和广泛的,老实说,我认为没有灵丹妙药或最佳实践。

For custom Content-Type(let's say you want to implement application/hal+json ), the official way and probably the most elegant way is to create custom output formatter .对于自定义 Content-Type(假设您要实现application/hal+json ),官方方式并且可能是最优雅的方式是创建自定义输出 formatter This way your actions won't know anything about the output format but you still can control the formatting behaviour inside your controllers thanks to dependency injection mechanism and scoped lifetime.这样你的动作就不会知道任何关于输出格式的信息,但你仍然可以控制控制器内部的格式化行为,这要归功于依赖注入机制和作用域生命周期。


1. Custom output formatters 1. 自定义输出格式器

This is the most popular way used by OData official C# libraries and json:api framework for ASP.Net Core .这是OData 官方 C# 库ASP.Net Core 的 json:api 框架使用的最流行的方式。 Probably the best way to implement hypermedia formats.可能是实现超媒体格式的最佳方式。

To control your custom output formatter from a controller you either have to create your own "context" to pass data between your controllers and custom formatter and add it to DI container with scoped lifetime:要从控制器控制自定义输出格式化程序,您必须创建自己的“上下文”以在控制器和自定义格式化程序之间传递数据,并将其添加到具有范围生命周期的 DI 容器中:

services.AddScoped<ApiContext>();

This way there will be only one instance of ApiContext per request.这样,每个请求将只有一个ApiContext实例。 You can inject it to both you controllers and output formatters and pass data between them.您可以将其注入控制器和输出格式化程序并在它们之间传递数据。

You can also use ActionContextAccessor and HttpContextAccessor and access your controller and action inside your custom output formatter.您还可以使用ActionContextAccessorHttpContextAccessor并在自定义输出格式化程序中访问您的控制器和操作。 To access controller you have to cast ActionContextAccessor.ActionContext.ActionDescriptor to ControllerActionDescriptor .要访问控制器,您必须将ActionContextAccessor.ActionContext.ActionDescriptorControllerActionDescriptor You can then generate links inside your output formatters using IUrlHelper and action names so the controller will be free from this logic.然后,您可以使用IUrlHelper和操作名称在您的输出格式化程序中生成链接,以便控制器不受此逻辑的影响。

IActionContextAccessor is optional and not added to the container by default, to use it in your project you have to add it to the IoC container. IActionContextAccessor是可选的,默认情况下不会添加到容器中,要在您的项目中使用它,您必须将其添加到 IoC 容器中。

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>()

Using services inside custom output formatter:在自定义输出格式化程序中使用服务:

You can't do constructor dependency injection in a formatter class.您不能在格式化程序类中进行构造函数依赖注入。 For example, you can't get a logger by adding a logger parameter to the constructor.例如,您无法通过向构造函数添加记录器参数来获取记录器。 To access services, you have to use the context object that gets passed in to your methods.要访问服务,您必须使用传递给您的方法的上下文对象。

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

Swashbuckle support : Swashbuckle 支持

Swashbuckle obviously won't generate a correct response example with this approach and the approach with filters. Swashbuckle 显然不会使用这种方法和使用过滤器的方法生成正确的响应示例。 You will probably have to create your custom document filter .您可能必须创建自定义 文档过滤器

Example: How to add pagination links :示例:如何添加分页链接

Usually paging, filtering is solved with specification pattern you will typically have some common model for the specification in your [Get] actions.通常分页,过滤是通过规范模式解决的,您通常会在[Get]操作中为规范提供一些通用模型。 You can then identify in your formatter if currently executed action is returning list of elements by it's parameter type or something else:然后,您可以在格式化程序中识别当前执行的操作是否通过其参数类型或其他类型返回元素列表:

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
   })
}

Advantages (or not) :优点(或不)

  • Your actions don't define the format, they know nothing about a format or how to generate links and where to put them.您的操作并没有定义格式,他们对格式一无所知,也不知道如何生成链接以及将链接放在哪里。 They know only of the result type, not the meta-data describing the result.他们只知道结果类型,而不知道描述结果的元数据。

  • Re-usable, you can easily add the format to other projects without worrying how to handle it in your actions.可重复使用,您可以轻松地将格式添加到其他项目中,而无需担心如何在您的操作中处理它。 Everything related to linking, formatting is handled under the hood.与链接、格式化相关的一切都在幕后处理。 No need for any logic in your actions.你的行动不需要任何逻辑。

  • Serialization implementation is up to you, you don't have to use Newtonsoft.JSON, you can use Jil for example.序列化实现取决于您,您不必使用 Newtonsoft.JSON,例如您可以使用Jil

Disadvantages :缺点

  • One disadvantage of this approach that it will only work with specific Content-Type.这种方法的一个缺点是它只适用于特定的 Content-Type。 So to support XML we'd need to create another custom output formatter with Content-Type like vnd.myapi+xml instead of vnd.myapi+json .因此,为了支持 XML,我们需要创建另一个带有 Content-Type 的自定义输出格式化程序,例如vnd.myapi+xml而不是vnd.myapi+json

  • We're not working directly with the action result我们不直接使用操作结果

  • Can be more complex to implement实施起来可能更复杂

2. Result filters 2. 结果过滤器

Result filters allow us to define some kind of behaviour that will execute before our action returns.结果过滤器允许我们定义某种行为,这些行为将在我们的动作返回之前执行。 I think of it as some form of post-hook.我认为它是某种形式的 post-hook。 I don't think it's the right place for wrapping our response.我认为这不是结束我们的回应的正确位置。

They can be applied per action or globally to all actions.它们可以应用于每个动作或全局应用于所有动作。

Personally, I wouldn't use it for this kind of thing but use it as a supplement for the 3rd option.就个人而言,我不会将它用于这种事情,而是将其用作第三个选项的补充。

Sample result filter wrapping the output:包装输出的示例结果过滤器:

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)
    {
    }
}

You can put the same logic in IActionFilter and it should work as well:您可以在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 };
        }
    }
}

This is the easiest way to wrap your responses especially if you already have the existing project with controllers.这是包装响应的最简单方法,特别是如果您已经拥有带有控制器的现有项目。 So if you care about time, choose this one.所以如果你在乎时间,就选这一款吧。

3. Explicitly formatting/wrapping your results in your actions 3. 在你的行动中明确格式化/包装你的结果

(The way I do it in my question) (我在我的问题中这样做的方式)

This is also used here:https://github.com/nbarbettini/BeautifulRestApi/tree/master/src to implement https://github.com/ionwg/ion-doc/blob/master/index.adoc personally I think this would be better suited in custom output formatter.这里也用这个:https ://github.com/nbarbettini/BeautifulRestApi/tree/master/src来实现https://github.com/ionwg/ion-doc/blob/master/index.adoc我个人认为这个将更适合自定义输出格式化程序。

This is probably the easiest way but it's also "sealing" your API to that specific format.这可能是最简单的方法,但它也将您的 API “密封”为该特定格式。 There are advantages to this approach but there can be some disadvantages too.这种方法有优点,但也有一些缺点。 For example, if you wanted to change the format of your API, you can't do it easily because your actions are coupled with that specific response model, and if you have some logic on that model in your actions, for example, you're adding pagination links for next and prev.例如,如果您想更改 API 的格式,则无法轻松完成,因为您的操作与该特定响应模型耦合,并且如果您在操作中对该模型有一些逻辑,例如,您重新为下一个和上一个添加分页链接。 You practically have to rewrite all your actions and formatting logic to support that new format.您实际上必须重写所有操作和格式逻辑以支持该新格式。 With custom output formatter you can even support both formats depending on the Content-Type header.使用自定义输出格式化程序,您甚至可以根据 Content-Type 标头支持两种格式。

Advantages:好处:

  • Works with all Content-Types, the format is an integral part of your API.适用于所有内容类型,格式是 API 不可或缺的一部分。
  • Swashbuckle works out of the box, when using ActionResult<T> (2.1+), you can also add [ProducesResponseType] attribute to your actions. Swashbuckle 开箱即用,当使用ActionResult<T> (2.1+) 时,您还可以将[ProducesResponseType]属性添加到您的操作中。

Disadvantages:缺点:

  • You can't control the format with Content-Type header.您无法使用Content-Type标头控制格式。 It always remains the same for application/json and application/xml . application/jsonapplication/xml始终保持不变。 (maybe it's advantage?) (也许这是优势?)
  • Your actions are responsible for returning the correctly formatted response.您的操作负责返回格式正确的响应。 Something like: return new ApiResponse(obj);类似于: return new ApiResponse(obj); or you can create extension method and call it like obj.ToResponse() but you always have to think about the correct response format.或者您可以创建扩展方法并像obj.ToResponse()一样调用它,但您始终必须考虑正确的响应格式。
  • Theoretically custom Content-Type like vnd.myapi+json doesn't give any benefit and implementing custom output formatter just for the name doesn't make sense as formatting is still responsibility of controller's actions.理论上,像vnd.myapi+json这样的自定义 Content-Type 没有任何好处,并且仅针对名称实现自定义输出格式化程序没有意义,因为格式化仍然是控制器操作的责任。

I think this is more like a shortcut for properly handling the output format.我认为这更像是正确处理输出格式的快捷方式。 I think following the single responsibility principle it should be the job for output formatter as the name suggests it formats the output.我认为遵循 单一职责原则应该是输出格式化程序的工作,顾名思义它格式化输出。

4. Custom middleware 4. 自定义中间件

The last thing you can do is a custom middleware, you can resolve IActionResultExecutor from there and return IActionResult like you would do in your MVC controllers.您可以做的最后一件事是自定义中间件,您可以从那里解析IActionResultExecutor并返回IActionResult就像您在 MVC 控制器中所做的那样。

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

You could also resolve IActionContextAccessor to get access to MVC's action context and cast ActionDescriptor to ControllerActionDescriptor if you need to access controller info.如果您需要访问控制器信息,您还可以解析IActionContextAccessor以访问 MVC 的操作上下文并将ActionDescriptorControllerActionDescriptor

Docs say:文档说:

Resource filters work like middleware in that they surround the execution of everything that comes later in the pipeline.资源过滤器的工作方式类似于中间件,因为它们围绕管道中后期出现的所有内容的执行。 But filters differ from middleware in that they're part of MVC, which means that they have access to MVC context and constructs.但是过滤器与中间件的不同之处在于它们是 MVC 的一部分,这意味着它们可以访问 MVC 上下文和构造。

But it's not entirely true, because you can access action context and you can return action results which is part of MVC from your middleware.但这并不完全正确,因为您可以访问操作上下文,并且可以从中间件返回作为 MVC 一部分的操作结果。


If you have anything to add, share your own experiences and advantages or disadvantages feel free to comment.如果您有什么要补充的,请分享您自己的经验和优点或缺点,随时发表评论。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM