信封 Odata 响应

Mat*_*ger 3 c# envelope odata .net-5

我将 Odata 添加到我的项目中,以便我可以使用 url-query-parameters,例如$filter. 使用演示类/控制器,输出现在如下所示:

{
  "@odata.context": "https://localhost:5001/api/v1/$metadata#WeatherForecast",
  "value": [
    {
      "Id": 1,
      "Date": "2021-05-22T14:00:18.9513586+02:00",
      "TemperatureC": 36,
      "Summary": "Sweltering"
    },
    {
      "Id": 2,
      "Date": "2021-05-23T14:00:21.6231763+02:00",
      "TemperatureC": 44,
      "Summary": "Chilly"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

到目前为止,效果很好。

使用我的 API 的前端团队现在想要像信封这样的东西..(是这样称呼的吗?)他们想要得到这样的结果:

{
    "data": {
        "type": "WeatherForecast",
        "count": 2,
        "items" : [
            {
              "Id": 1,
              "Date": "2021-05-22T14:00:18.9513586+02:00",
              "TemperatureC": 36,
              "Summary": "Sweltering"
            },
            {
              "Id": 2,
              "Date": "2021-05-23T14:00:21.6231763+02:00",
              "TemperatureC": 44,
              "Summary": "Chilly"
            }
        ]
    },
    "error": null
}
Run Code Online (Sandbox Code Playgroud)

我有办法做到这一点吗?直接使用 OData 还是以其他方式?我不能简单地更改返回类型,因为 OData 需要 aIQueryable<>作为返回类型。这就是我尝试过的,但 Odata 无法过滤。

Ibr*_*eem 6

有多种方法可以实现此目的,但在直接跳到答案之前,我必须提到如果您自定义 API 返回的响应。由于非常规的响应格式,某些 OData 集成(例如 PowerBI、OData 客户端等)可能无法按预期工作。但是,如果您不打算将任何 OData 客户端与您的 API 集成,那么这对您来说应该不成问题。

另一件事是,也许您应该与公司的前端开发人员进行讨论,仅当他们在响应中收到非成功状态代码时才查找错误。他们应该期待 REST 标准错误响应(即ProblemDetails)。他们建议的响应格式在GraphQL 世界中更为常见

回到主题,要自定义 OData 响应,您可以通过控制器的操作方法来完成。

public IActionResult Get(ODataQueryOptions<WeatherForecast> odataOptions)
    {
        var data = odataOptions.ApplyTo(_dbContext.Forecasts);
        var odataFeature = HttpContext.ODataFeature();

        var response = new WeatherApiEnvelope()
        {
            Data = new WeatherApiDataEnvelope()
            {
                Count = odataFeature.TotalCount,
                Items = data,
                Type = odataFeature.Path.GetEdmType().AsElementType().FullTypeName()
            }
        };

        return Ok(response);
    }
Run Code Online (Sandbox Code Playgroud)

在此方法中,您必须删除 EnableQueryAttribute(如果存在)并返回 IActionResult 而不是 IQueryable,并将 ODataQueryOptions 作为参数添加到操作方法)。ODataQueryOptions 是一个特定于 OData 的模型,它将携带来自查询字符串的查询信息,并且它具有 ApplyTo() 方法,该方法将 OData 查询(例如,过滤器、投影等)应用于 IQueryable 。

注意: OData 会执行额外的工作来获取总计数(如果您应用 $count=true),因为它必须在不考虑数据分页或($skip 和 $top)查询选项的情况下执行此操作。因此,当您调用 ApplyTo() 函数时,它将在 HttpContext 功能内的 IODataFeature 中设置总计数。

就我个人而言,我喜欢这种方法,因为它可以更好地控制我可以从方法返回的内容(例如,错误响应)。如果您想通过利用OutputFormatters将 IQueryable 保留为返回类型,还有另一种方法。简而言之,OutputFormatters在action方法返回(包括过滤器)之后执行,其唯一目的是格式化响应并将其写入响应流中。

当您在 Startup 中调用 AddOData() 时,它会注入 OData 所需的服务,包括 ODataOutputFormatter,它将格式化和序列化 OData 响应。在下面的示例中,我将通过添加新的 ODataOutputFormatter 来覆盖默认行为。

    //Action Method
    [EnableQuery]
    public IQueryable<WeatherForecast> Get()
    {
        return _dbContext.Forecasts;
    }
Run Code Online (Sandbox Code Playgroud)

该操作方法由 EnableQueryAttribute 注释,它与我们在前面的示例中所做的完全一样(将查询应用于 IQueryable)。

注意:我的实现运行需要此注释,但您也可以更进一步,自己在输出格式化程序中应用查询。

public class WeatherforecastCustomOutputFormatter : ODataOutputFormatter
{
    public WeatherforecastCustomOutputFormatter() : base(new List<ODataPayloadKind>
    {
        ODataPayloadKind.ResourceSet,
        ODataPayloadKind.Resource
    })
    {
        SupportedMediaTypes.Add(MediaTypeNames.Application.Json);
        SupportedEncodings.Add(Encoding.UTF8);
    }

    public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var odataFeature = context.HttpContext.ODataFeature();
        var response = new WeatherApiEnvelope()
        {
            Data = new WeatherApiDataEnvelope()
            {
                Count = odataFeature.TotalCount,
                Items = context.Object,
                Type = odataFeature.Path.GetEdmType().AsElementType().FullTypeName()
            }
        };

        return context.HttpContext.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}
Run Code Online (Sandbox Code Playgroud)

此自定义输出格式化程序继承自 ODataOutputFormatter 并重写 WriteResponseBodyAsync 以实现所需的行为。

注意:在 OData 中,有一个 ODataPayloadKind 概念,您可以在此处阅读,对于此格式化程序的范围,我仅包含 ResourceSet 和 Resource,它们应该涵盖返回对象列表或单个对象的需要。现在我们应该在 AddControllers() 中注入这个输出格式化程序。

public void ConfigureServices(IServiceCollection services)
    {
        services.AddOData(o => o.AddModel(GetEdmModel()).Filter().Select().OrderBy().Expand().SkipToken().Count());
        services.AddControllers(options =>
        {
            options.OutputFormatters.Insert(0, new WeatherforecastCustomOutputFormatter());
        });
    }
Run Code Online (Sandbox Code Playgroud)

在这里,我将自定义的 OutputFormatter 插入第一个索引中,使其在现有的 ODataOutputFormatter 之前运行,这不会删除现有的 ODataOutputFormatter,但会覆盖它,因为它将首先运行来处理请求。

注意:在这种情况下,在 AddControllers 之前调用 AddOData() 非常重要。而且我的格式化程序实现仅用于演示,您应该处理更多情况(例如,检查空值、处理错误、使用序列化选项等)