信封Odata响应
Envelope Odata-response
我将 Odata 添加到我的项目中,因此我可以使用 url-查询参数,如 $filter
。
使用 demo-class/controller,输出现在如下所示:
{
"@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"
}
]
}
到目前为止,很好,有效。
消耗我 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
}
有没有办法做到这一点?直接使用 OData 还是以其他方式使用?
我不能简单地更改 return 类型,因为 OData 需要 IQueryable<>
作为 return 类型。这就是我尝试过的方法,但是 Odata 无法过滤。
事实是,您在上面发布的内容有效 - 这是 Odata 的包络响应。
Envelop 表示对象包裹在 odata
或 $odata
标签中,并包含 json 数组中的响应值:
"Value": [{..}{..}]
您的客户期望的是
形式的信封但简单的数据对象列表
{"data":{[{..}{..}]},"error":..}
如果你想获得普通列表,你必须相应地使用 $select 属性,例如:
../api/home/GetSomeList?$select=param1,param2,param3
有几种方法可以实现这一点,但在我直接跳到答案之前,我必须提到,如果您自定义 return 由 API 编辑的响应。由于非常规的响应格式,某些 OData 集成(例如 PowerBI、OData 客户端等)可能无法按预期工作。但是,如果您不打算将任何 OData 客户端与您的 API 集成,这对您来说应该不是问题。
另一件事是,也许您应该与贵公司的前端开发人员进行讨论,以便仅当他们在响应中收到非成功状态代码时才查找错误。他们应该期待 REST 标准错误响应(即 ProblemDetails). The response format they're suggesting is more common in the GraphQL world
回到主题,自定义OData响应,可以通过controller的action方法实现。
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);
}
在这种方法中,您必须删除 EnableQueryAttribute(如果存在)和 returnIActionResult 而不是 IQueryable,并将 ODataQueryOptions 作为参数添加到操作方法中)。 ODataQueryOptions 是一个特定于 OData 的模型,它将从查询字符串中携带查询信息,并且它具有 ApplyTo() 方法,该方法将 OData 查询(例如,过滤器、投影等)应用于 IQueryable .
注意: OData 做了额外的工作来获取总计数(如果您应用 $count=true),因为它必须在不考虑数据分页或($ skip 和 $top) 查询选项。因此,当您调用 ApplyTo() 函数时,它将在 HttpContext 功能中的 IODataFeature 中设置总计数。
就我个人而言,我喜欢这种方法,因为它使我可以 return 从方法中更好地控制什么(例如,错误响应)。如果您想通过利用 OutputFormatters 将 IQueryable 保持为 return 类型,还有另一种方法。简而言之,OutputFormatters 在动作方法 returns(包括过滤器)之后执行,其唯一目的是格式化响应并将其写入响应流。
当您在 Startup 中调用 AddOData() 时,它会为 OData 注入必要的服务,包括 ODataOutputFormatter,它将格式化和序列化 OData 响应。在下面的示例中,我将通过添加新的 ODataOutputFormatter 来覆盖默认行为。
//Action Method
[EnableQuery]
public IQueryable<WeatherForecast> Get()
{
return _dbContext.Forecasts;
}
操作方法由 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));
}
}
此自定义输出格式化程序继承自 ODataOutputFormatter 并覆盖 WriteResponseBodyAsync 以实现所需的行为。
注意: 在 OData 中有 ODataPayloadKind 的概念,您可以阅读 here,对于这个格式化程序的范围,我只包括 ResourceSet 和 Resource 应该涵盖需要 return 对象列表或单个对象。现在我们应该在我们的 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());
});
}
这里我在第一个索引中插入我的自定义 OutputFormatter 以使其 运行 在现有的之前,这不会删除现有的 ODataOutputFormatter 但会覆盖它,因为它会 运行 首先处理请求。
注意: 在这种情况下,在 AddControllers 之前调用 AddOData() 很重要。而且我对格式化程序的实现仅用于演示,您应该处理更多情况(例如,检查空值、处理错误、使用序列化选项等)
我将 Odata 添加到我的项目中,因此我可以使用 url-查询参数,如 $filter
。
使用 demo-class/controller,输出现在如下所示:
{
"@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"
}
]
}
到目前为止,很好,有效。
消耗我 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
}
有没有办法做到这一点?直接使用 OData 还是以其他方式使用?
我不能简单地更改 return 类型,因为 OData 需要 IQueryable<>
作为 return 类型。这就是我尝试过的方法,但是 Odata 无法过滤。
事实是,您在上面发布的内容有效 - 这是 Odata 的包络响应。
Envelop 表示对象包裹在 odata
或 $odata
标签中,并包含 json 数组中的响应值:
"Value": [{..}{..}]
您的客户期望的是
形式的信封但简单的数据对象列表{"data":{[{..}{..}]},"error":..}
如果你想获得普通列表,你必须相应地使用 $select 属性,例如:
../api/home/GetSomeList?$select=param1,param2,param3
有几种方法可以实现这一点,但在我直接跳到答案之前,我必须提到,如果您自定义 return 由 API 编辑的响应。由于非常规的响应格式,某些 OData 集成(例如 PowerBI、OData 客户端等)可能无法按预期工作。但是,如果您不打算将任何 OData 客户端与您的 API 集成,这对您来说应该不是问题。
另一件事是,也许您应该与贵公司的前端开发人员进行讨论,以便仅当他们在响应中收到非成功状态代码时才查找错误。他们应该期待 REST 标准错误响应(即 ProblemDetails). The response format they're suggesting is more common in the GraphQL world
回到主题,自定义OData响应,可以通过controller的action方法实现。
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);
}
在这种方法中,您必须删除 EnableQueryAttribute(如果存在)和 returnIActionResult 而不是 IQueryable,并将 ODataQueryOptions 作为参数添加到操作方法中)。 ODataQueryOptions 是一个特定于 OData 的模型,它将从查询字符串中携带查询信息,并且它具有 ApplyTo() 方法,该方法将 OData 查询(例如,过滤器、投影等)应用于 IQueryable .
注意: OData 做了额外的工作来获取总计数(如果您应用 $count=true),因为它必须在不考虑数据分页或($ skip 和 $top) 查询选项。因此,当您调用 ApplyTo() 函数时,它将在 HttpContext 功能中的 IODataFeature 中设置总计数。
就我个人而言,我喜欢这种方法,因为它使我可以 return 从方法中更好地控制什么(例如,错误响应)。如果您想通过利用 OutputFormatters 将 IQueryable 保持为 return 类型,还有另一种方法。简而言之,OutputFormatters 在动作方法 returns(包括过滤器)之后执行,其唯一目的是格式化响应并将其写入响应流。
当您在 Startup 中调用 AddOData() 时,它会为 OData 注入必要的服务,包括 ODataOutputFormatter,它将格式化和序列化 OData 响应。在下面的示例中,我将通过添加新的 ODataOutputFormatter 来覆盖默认行为。
//Action Method
[EnableQuery]
public IQueryable<WeatherForecast> Get()
{
return _dbContext.Forecasts;
}
操作方法由 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));
}
}
此自定义输出格式化程序继承自 ODataOutputFormatter 并覆盖 WriteResponseBodyAsync 以实现所需的行为。
注意: 在 OData 中有 ODataPayloadKind 的概念,您可以阅读 here,对于这个格式化程序的范围,我只包括 ResourceSet 和 Resource 应该涵盖需要 return 对象列表或单个对象。现在我们应该在我们的 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());
});
}
这里我在第一个索引中插入我的自定义 OutputFormatter 以使其 运行 在现有的之前,这不会删除现有的 ODataOutputFormatter 但会覆盖它,因为它会 运行 首先处理请求。
注意: 在这种情况下,在 AddControllers 之前调用 AddOData() 很重要。而且我对格式化程序的实现仅用于演示,您应该处理更多情况(例如,检查空值、处理错误、使用序列化选项等)