问题描述
我想创建自定义 JSON 格式,它将响应包装在数据中并返回 Content-Type
I'd like to create custom JSON format, that would wrap the response in data and would return Content-Type like
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;
}
}
我想返回如下响应:
{
"data": { ... },
"pagination": { ... },
"someothermetadata": { ... }
}
但是必须以某种方式将分页添加到我的控制器操作中的元数据中,当然这里有一些关于内容协商的文章:https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-2.1 但我仍然想确保我走在正确的轨道上.
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.
如果这将使用我的自定义格式化程序在后台处理,那么我将如何向它添加像分页这样的元数据,以便在数据"之外而不是在其中?
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.
上述方法的一个优点或缺点是它适用于所有序列化程序 xml、json、yaml 等.通过使用自定义格式化程序,它可能只适用于 json,我需要创建几个不同的格式化程序来支持所有我想要的格式.
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.
推荐答案
好的,在花了一些时间使用 ASP.NET Core 之后,我基本上可以想到 4 种方法来解决这个问题.这个主题本身非常复杂和广泛,老实说,我不认为有灵丹妙药或最佳实践.
Okay, after spending some good amount of time with ASP.NET Core there are basically 4 ways I can think of to solve this. 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.
对于自定义的Content-Type(假设你要实现application/hal+json
),官方的方式,也许是最优雅的方式是创建自定义输出格式化程序.这样一来,您的操作就不会知道任何关于输出格式的信息,但由于依赖注入机制和作用域生命周期,您仍然可以控制控制器内部的格式化行为.
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. 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.
这是 OData 官方 C# 库最常用的方式 和 json: ASP.Net Core 的 api 框架.可能是实现超媒体格式的最佳方式.
This is the most popular way used by OData official C# libraries and json:api framework for ASP.Net Core. Probably the best way to implement hypermedia formats.
要从控制器控制您的自定义输出格式化程序,您必须创建自己的上下文";在控制器和自定义格式化程序之间传递数据并将其添加到具有作用域生命周期的 DI 容器中:
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:
services.AddScoped();
这样,每个请求只有一个 ApiContext
实例.您可以将它注入到您的控制器和输出格式化程序中,并在它们之间传递数据.
This way there will be only one instance of ApiContext
per request. You can inject it to both you controllers and output formatters and pass data between them.
您还可以使用 ActionContextAccessor
和 HttpContextAccessor
并在自定义输出格式化程序中访问您的控制器和操作.要访问控制器,您必须将 ActionContextAccessor.ActionContext.ActionDescriptor
转换为 ControllerActionDescriptor
.然后,您可以使用 IUrlHelper
和操作名称在输出格式化程序中生成链接,这样控制器就不会受到这种逻辑的影响.
You can also use ActionContextAccessor
and HttpContextAccessor
and access your controller and action inside your custom output formatter. To access controller you have to cast ActionContextAccessor.ActionContext.ActionDescriptor
to ControllerActionDescriptor
. You can then generate links inside your output formatters using IUrlHelper
and action names so the controller will be free from this logic.
IActionContextAccessor
是可选的,默认情况下不会添加到容器中,要在项目中使用它,您必须将其添加到 IoC 容器中.
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.
services.AddSingleton
在自定义输出格式化程序中使用服务:
您不能在格式化程序类中进行构造函数依赖注入.例如,您无法通过向构造函数添加记录器参数来获取记录器.要访问服务,您必须使用传递给您的方法的上下文对象.
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
Swashbuckle 支持:
Swashbuckle 显然不会使用这种方法和使用过滤器的方法生成正确的响应示例.您可能需要创建自定义 文档过滤器.
Swashbuckle obviously won't generate a correct response example with this approach and the approach with filters. You will probably have to create your custom document filter.
示例:如何添加分页链接:
通常分页,过滤是通过规范模式来解决的,你通常会有一些通用的规范模型在您的 [Get]
操作中.然后,您可以在格式化程序中识别当前执行的操作是否通过其参数类型或其他内容返回元素列表:
Usually paging, filtering is solved with specification pattern you will typically have some common model for the specification in your [Get]
actions. 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
})
}
优点(或没有):
您的操作没有定义格式,他们对格式或如何生成链接以及将链接放置在何处一无所知.他们只知道结果类型,而不知道描述结果的元数据.
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.
序列化实现由你决定,你不必使用Newtonsoft.JSON,你可以使用吉尔 例如.
Serialization implementation is up to you, you don't have to use Newtonsoft.JSON, you can use Jil for example.
缺点:
这种方法的一个缺点是它只适用于特定的 Content-Type.因此,为了支持 XML,我们需要创建另一个自定义输出格式化程序,其 Content-Type 类似于
vnd.myapi+xml
而不是vnd.myapi+json
.
我们不直接处理操作结果
We're not working directly with the action result
实现起来可能更复杂
结果过滤器允许我们定义某种将在我们的操作返回之前执行的行为.我认为它是某种形式的后钩.我认为这不是包装我们回复的正确位置.
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. I don't think it's the right place for wrapping our response.
它们可以应用于每个操作或全局应用于所有操作.
They can be applied per action or globally to all actions.
就我个人而言,我不会将它用于这种事情,而是将其用作第 3 个选项的补充.
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)
{
}
}
您可以将相同的逻辑放入 IActionFilter
中,它应该也可以工作:
You can put the same logic in IActionFilter
and it should work as well:
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.
(我在问题中的做法)
这里也用到这个:https://github.com/nbarbettini/BeautifulRestApi/tree/master/src 来实现 https://github.com/ionwg/ion-doc/blob/master/index.adoc 我个人认为这将更适合自定义输出格式化程序.
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.
这可能是最简单的方法,但它也是密封"的.您的 API 到该特定格式.这种方法有优点,但也有一些缺点.例如,如果您想更改 API 的格式,则不能轻易做到,因为您的操作与特定的响应模型相结合,并且如果您的操作中有一些关于该模型的逻辑,例如,您为下一个和上一个重新添加分页链接.您实际上必须重写所有操作和格式化逻辑以支持该新格式.使用自定义输出格式化程序,您甚至可以根据 Content-Type 标头支持这两种格式.
This is probably the easiest way but it's also "sealing" your API to that specific format. 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. 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,格式是 API 不可或缺的一部分.
- Swashbuckle 开箱即用,使用
ActionResult<T>
(2.1+) 时,您还可以将[ProducesResponseType]
属性添加到您的操作中.
- Works with all Content-Types, the format is an integral part of your API.
- Swashbuckle works out of the box, when using
ActionResult<T>
(2.1+), you can also add[ProducesResponseType]
attribute to your actions.
缺点:
- 您无法使用
Content-Type
标头控制格式.application/json
和application/xml
始终保持相同.(也许是优势?) - 您的操作负责返回格式正确的响应.类似:
return new ApiResponse(obj);
或者您可以创建扩展方法并像obj.ToResponse()
一样调用它,但您始终必须考虑正确的响应格式. - 从理论上讲,像
vnd.myapi+json
这样的自定义 Content-Type 并没有带来任何好处,并且仅仅为名称实现自定义输出格式化程序没有意义,因为格式化仍然是控制器操作的责任.
- You can't control the format with
Content-Type
header. It always remains the same forapplication/json
andapplication/xml
. (maybe it's advantage?) - Your actions are responsible for returning the correctly formatted response. Something like:
return new ApiResponse(obj);
or you can create extension method and call it likeobj.ToResponse()
but you always have to think about the correct response format. - 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.
我认为这更像是正确处理输出格式的捷径.我认为遵循 单一责任原则 它应该是输出格式化程序的工作,顾名思义它格式化输出.
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.
您可以做的最后一件事是自定义中间件,您可以从那里解析 IActionResultExecutor
并返回 IActionResult
,就像在 MVC 控制器中一样.
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.
https://github.com/aspnet/Mvc/issues/7238#issuecomment-357391426
如果您需要访问控制器信息,您还可以解析 IActionContextAccessor
以访问 MVC 的操作上下文并将 ActionDescriptor
转换为 ControllerActionDescriptor
.
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.
文档说:
资源过滤器的工作方式类似于中间件,因为它们围绕管道中稍后出现的所有内容的执行.但过滤器与中间件的不同之处在于它们是 MVC 的一部分,这意味着它们可以访问 MVC 上下文和构造.
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 的一部分.
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.
如果你有什么要补充的,分享你自己的经验和优缺点,欢迎评论.
If you have anything to add, share your own experiences and advantages or disadvantages feel free to comment.
这篇关于ASP.NET Core 在 Web API 中处理自定义响应/输出格式的方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!