
内容协商是一个允许客户端和服务器就 Web 请求中交换的最佳数据格式达成一致的过程。它是 Web API 开发的一个重要功能,因为它可以创建灵活且可互操作的 Web 服务,这些服务可以支持多种媒体类型。在本文中,我们将探讨内容协商的工作原理、它的重要性以及如何在 .NET 6 中实现它。.NET 6 是 .NET 的最新版本,为 Web API 开发提供了许多改进和新功能。
您可以在此找到本文的源代码:https://download.youkuaiyun.com/download/hefeng_aspnet/89997576
了解内容协商
内容协商基于以下原则:客户端和服务器应就其可以发送和接收的媒体类型传达各自的偏好和功能。媒体类型(也称为 MIME 类型)是一种标准化的识别数据格式和结构的方法,例如application/json、text/html、image/png等。内容协商允许客户端和服务器根据可用性、质量、效率和用户偏好等因素,协商出最适合每个请求和响应的媒体类型。
内容协商主要有两个方面:客户端方面和服务器方面。客户端方面涉及使用以下一个或多个 HTTP 标头指定响应所需的媒体类型:
Accept:表示响应可接受的媒体类型,以及质量因素等可选参数(例如Accept: application/json;q=0.9, text/plain;q=0.8)。Accept-Charset:表示响应可接受的字符集(例如Accept-Charset: utf-8, iso-8859-1)。Accept-Encoding:表示响应可接受的内容编码(例如Accept-Encoding: gzip, deflate)。Accept-Language:表示响应所首选的自然语言(例如Accept-Language: en-US, fr-CA)。
服务器视角涉及使用以下一个或多个 HTTP 标头为响应提供适当的媒体类型:
Content-Type:指示发送给客户端的实体主体的媒体类型(例如Content-Type: application/json)。Content-Encoding:表示应用于实体主体的内容编码(例如Content-Encoding: gzip)。Content-Language:表示实体主体的自然语言(例如Content-Language: en-US)。
服务器还可以使用其他 HTTP 标头来提供有关可用媒体类型的其他信息,例如:
Vary:表示使用哪些请求标头来选择响应(例如Vary: Accept)。Content-Disposition:表示如何向用户呈现响应(例如Content-Disposition: attachment; filename="report.pdf")。Link:提供资源其他表现形式的链接(例如Link: <http://example.com/report.json>; rel="alternate"; type="application/json")。
支持的媒体类型取决于客户端和服务器的实现。不过,Web API 开发的一些常见媒体类型包括:
application/json:一种基于 JavaScript 对象表示法 (JSON) 的轻量级且人类可读的表示结构化数据的格式。application/xml:一种基于可扩展标记语言(XML)的广泛使用的表示结构化数据的格式。text/plain:一种表示非结构化文本数据的简单格式。text/html:一种基于超文本标记语言(HTML)的表示超文本文档的格式。text/csv:一种基于逗号分隔值(CSV)的表示表格数据的格式。image/*:用于表示图像数据的一组格式,例如image/png、image/jpeg、image/gif等。
Web API 中的内容协商
内容协商适合 Web API 架构,它允许客户端和服务器以不同的格式交换数据,而无需更改 Web 服务的底层逻辑或功能。这有几个主要好处,例如:
- 灵活性:内容协商使 Web 服务能够支持多种媒体类型,并满足不同客户的需求和偏好。例如,提供产品信息的 Web 服务可以返回 JSON 格式的数据(用于 Web 应用程序)、XML 格式的数据(用于旧式系统)、HTML 格式的数据(用于浏览器)或 CSV 格式的数据(用于电子表格)。
- 互操作性:内容协商有助于不同系统和平台之间的通信,这些系统和平台可能具有不同的功能和对数据格式的期望。例如,使用来自另一个 Web 服务的数据的 Web 服务可以协商出对双方来说最佳的媒体类型,并避免兼容性问题。
- 效率:内容协商允许通过为每种情况选择最合适的媒体类型来优化数据传输和处理。例如,返回大量数据的 Web 服务可以使用压缩技术(如 gzip)来减小响应的大小并提高网络性能。
在 .NET 6 中实现内容协商
.NET 6 是 .NET 的最新版本,它提供了一个统一的平台,用于跨不同领域(例如 Web、移动、桌面、云、游戏和物联网)构建现代应用程序。.NET 6 为 Web API 开发提供了许多改进和新功能,例如:
- 最少 API:一种使用最少代码和配置创建轻量级且富有表现力的 Web 服务的新方法。
- 热重载:无需重新启动应用程序或丢失应用程序状态即可更改代码的功能。
- Blazor:一个使用 C# 而不是 JavaScript 构建交互式 Web UI 的框架。
- gRPC:一个用于构建远程过程调用 (RPC) 服务的高性能、跨平台、开源框架。
- OpenAPI:使用机器可读和人类可读的格式描述和记录 Web 服务的标准。
在本节中,我们将了解如何使用基本的 Web API 项目在 .NET 6 中实现内容协商。我们将使用 Visual Studio Code (VS Code) 作为代码编辑器,使用 .NET 命令行界面 (CLI) 作为创建和运行项目的工具。我们还将使用 Postman 作为测试 Web 服务的工具。
设置基本的 .NET 6 Web API 项目
要创建一个基本的.NET 6 Web API 项目,我们需要遵循以下步骤:
- 在我们的机器上安装最新版本的.NET 6 SDK。
- 在我们的机器上安装最新版本的VS Code。
- 安装VS Code 的C# Dev Kit 。
- 打开 VS Code 并为我们的项目创建一个新文件夹(例如
ContentNegotiationDemo)。 - 在 VS Code 中打开终端并运行以下命令,使用
webapi模板创建一个新的 Web API 项目:
dotnet new webapi -n ContentNegotiationDemo
- 运行以下命令恢复项目的依赖关系:
dotnet restore
- 运行以下命令来运行项目:
dotnet run
- 打开浏览器并导航到
https://localhost:5001/WeatherForecast以查看 JSON 格式的 Web 服务的默认响应。
在 Program.cs 中配置内容协商
要在我们的 Web API 项目中配置内容协商,我们需要修改Program.cs包含 Web 服务的配置和中间件组件的文件。我们需要进行以下更改:
using在文件顶部 添加以下语句。
using System.Buffers;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
添加以下代码以AddControllers使用内容协商的自定义选项配置服务:
builder.Services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
// Add custom media type formatter for CSV format
options.OutputFormatters.Add(new CsvOutputFormatter());
options.OutputFormatters.Add(new HtmlOutputFormatter());
})
.AddXmlSerializerFormatters();
- 保存文件。
使用 MediaTypeFormatter 处理媒体类型
为了在我们的 Web API 项目中处理不同的媒体类型,我们需要创建从 继承的自定义类MediaTypeFormatter,这是一个抽象类,提供以各种格式序列化和反序列化数据的功能。在本节中,我们将创建两个自定义类:CsvOutputFormatter和HtmlOutputFormatter,分别处理 CSV 和 HTML 格式。
CsvOutputFormatter
要创建用于处理 CSV 格式的自定义类,我们需要遵循以下步骤:
- 在我们的项目文件夹中创建一个名为的新文件夹
Formatters。 CsvOutputFormatter.cs在文件夹中创建一个名为的新文件Formatters。- 在文件中添加以下代码:
using System.Text;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
namespace ContentNegotiationDemo.Formatters
{
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv"));
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
return typeof(IEnumerable<WeatherForecast>).IsAssignableFrom(type);
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var data = context.Object as IEnumerable<WeatherForecast>;
if (data == null)
return;
using (var buffer = new MemoryStream())
using (var writer = new StreamWriter(buffer, selectedEncoding))
{
var csv = data.ToCsvString(',', true);
writer.Write(csv);
writer.Flush();
buffer.Position = 0;
await buffer.CopyToAsync(response.Body);
}
}
}
}
CsvHelper
using System.Text;
namespace ContentNegotiationDemo;
// Copied from https://stackoverflow.com/a/72553753
public static class CsvHelper
{
/// <summary>
/// Converts the given enumerable into a CSV string. Optionally, specify the delimiter or include headers.
/// For enumerables of primitive types, it will convert them to a single-line CSV. Headers are not valid for this case.
/// For enumerables of complex types, it will inspect the properties and convert each item into a line of the CSV.
/// Which properties are included/excluded and the header names in the resulting CSV can be controlled.
/// Note: Headers and values will only be double-quoted if necessary as per RFC4180.
/// </summary>
/// <typeparam name="T">The type of the enumerable.</typeparam>
/// <param name="enumerable">The enumerable to turn into a CSV.</param>
/// <param name="delimiter">The delimiter.</param>
/// <param name="includeHeaders">Whether to include headers.</param>
/// <param name="propertiesToInclude">Properties from the objects given to include. If left null, all properties will be included. This does not apply for enumerables of primitive types.</param>
/// <param name="propertiesToExclude">Properties to exclude from the DataTable, if any. This does not apply for enumerables of primitive types.</param>
/// <param name="propertyNameHeaderMap">A map that will be used to translate the property names to the headers that should appear in the CSV. This does not apply for enumerables of primitive types.</param>
/// <returns>A CSV representation of the objects in the enumeration.</returns>
public static string ToCsvString<T>(
this IEnumerable<T> enumerable,
char delimiter = ',',
bool includeHeaders = false,
IEnumerable<string> propertiesToInclude = null,
IEnumerable<string> propertiesToExclude = null,
Dictionary<string, string> propertyNameHeaderMap = null)
{
if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));
var type = enumerable.FirstOrDefault()?.GetType();
if (type == null) return "";
if (type.IsSimpleType())
return string.Join(delimiter, enumerable.Select(i => escapeCsvValue(i?.ToString(), delimiter)));
var csvBuilder = new StringBuilder();
var allProperties = type.GetProperties();
var propsToIncludeSet = (propertiesToInclude ?? allProperties.Select(p => p.Name))
.Except(propertiesToExclude ?? Enumerable.Empty<string>())
.ToHashSet();
var properties = allProperties
.Where(p => propsToIncludeSet.Contains(p.Name))
.ToList();
if (includeHeaders)
{
var headerNames = properties
.Select(p => escapeCsvValue(propertyNameHeaderMap == null ? p.Name : propertyNameHeaderMap.GetValueOrDefault(p.Name) ?? $"{nameof(propertyNameHeaderMap)} was missing a value for property {p.Name}", delimiter));
csvBuilder.AppendLine(string.Join(delimiter, headerNames));
}
foreach (var item in enumerable)
{
var vals = properties.Select(p => escapeCsvValue(p.GetValue(item, null)?.ToString(), delimiter));
var line = string.Join(delimiter, vals);
csvBuilder.AppendLine(line);
}
return csvBuilder.ToString();
//Function to escape a value for use in CSV. Per RFC4180, if the delimiter, newline, or double quote is present in the value it must be double quoted. If the value contains double quotes they must be escaped.
static string escapeCsvValue(string s, char delimiter)
{
return s == null ? null
: s.Any(c => c == delimiter || c == '"' || c == '\r' || c == '\n') ? $"\"{s.Replace("\"", "\"\"")}\""
: s;
}
}
/// <summary>
/// Whether the given type is a "simple" type. Eg, a built in CLR type to represent data.
/// This includes all integral types, floating points types, DateTime, DateOnly, decimal, and Guid.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="unwrapNullable">Whether the type inside a nullable type should be checked.</param>
/// <returns>Whether the type was a simple type.</returns>
/// <exception cref="ArgumentNullException">If type was empty.</exception>
public static bool IsSimpleType(this Type type, bool unwrapNullable = true)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (unwrapNullable) type = Nullable.GetUnderlyingType(type) ?? type;
return type.IsPrimitive
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(DateOnly)
|| type == typeof(decimal)
|| type == typeof(Guid)
;
}
}
此类继承自TextOutputFormatter,它是用于写入文本数据的格式化程序的基类。它重写了两个方法:CanWriteType和WriteResponseBodyAsync。该CanWriteType方法检查数据类型是否可以序列化为 CSV 格式,在本例中是实现IEnumerable接口的任何类型。该WriteResponseBodyAsync方法使用内存缓冲区和流写入器将数据以 CSV 格式写入响应主体流。它使用第一项的属性名称写入标题行,然后使用每个项的属性值写入数据行,以逗号分隔。它还在构造函数中设置支持的媒体类型和编码。
HtmlOutputFormatter
要创建用于处理 HTML 格式的自定义类,我们需要遵循以下步骤:
HtmlOutputFormatter.cs在文件夹中创建一个名为的新文件Formatters。- 在文件中添加以下代码:
using System.Text;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
namespace ContentNegotiationDemo.Formatters
{
public class HtmlOutputFormatter : TextOutputFormatter
{
public HtmlOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/html"));
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
return typeof(IEnumerable<WeatherForecast>).IsAssignableFrom(type);
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var data = context.Object as IEnumerable<WeatherForecast>;
if (data == null)
return;
using (var buffer = new MemoryStream())
using (var writer = new StreamWriter(buffer, selectedEncoding))
{
writer.WriteLine("<!DOCTYPE html>");
writer.WriteLine("<html lang=\"en\">");
writer.WriteLine("<head>");
writer.WriteLine("<meta charset=\"UTF-8\">");
writer.WriteLine("<title>Content Negotiation Demo</title>");
writer.WriteLine("<style>");
writer.WriteLine("table, th, td { border: 1px solid black; border-collapse: collapse; }");
writer.WriteLine("th, td { padding: 5px; }");
writer.WriteLine("</style>");
writer.WriteLine("</head>");
writer.WriteLine("<body>");
var htmlTable = data.ToHtmlTable();
writer.WriteLine(htmlTable);
writer.WriteLine("</body>");
writer.WriteLine("</html>");
writer.Flush();
buffer.Position = 0;
await buffer.CopyToAsync(response.Body);
}
}
}
}
HtmlHelper
using System.Text;
namespace ContentNegotiationDemo;
// Copied from https://codereview.stackexchange.com/a/211601
public static class HtmlHelper
{
public static string ToHtmlTable<T>(this IEnumerable<T> enums)
{
var type = typeof(T);
var props = type.GetProperties();
var html = new StringBuilder("<table>");
//Header
html.Append("<thead><tr>");
foreach (var p in props)
html.Append("<th>" + p.Name + "</th>");
html.Append("</tr></thead>");
//Body
html.Append("<tbody>");
foreach (var e in enums)
{
html.Append("<tr>");
props.Select(s => s.GetValue(e)).ToList().ForEach(p => {
html.Append("<td>" + p + "</td>");
});
html.Append("</tr>");
}
html.Append("</tbody>");
html.Append("</table>");
return html.ToString();
}
}
此类继承自TextOutputFormatter,它是写入文本数据的格式化程序的基类。它重写了两个方法:CanWriteType和WriteResponseBodyAsync。该CanWriteType方法检查数据类型是否可以序列化为 HTML 格式,在本例中是实现IEnumerable接口的任何类型。该WriteResponseBodyAsync方法使用内存缓冲区和流写入器将数据以 HTML 格式写入响应主体流。它写入带有标题和样式的 HTML 文档,然后写入 HTML 表,其中标题行包含第一项的属性名称,数据行包含每项的属性值。它还在构造函数中设置支持的媒体类型和编码。
内容协商的最佳实践
内容协商是一项强大的功能,可以增强我们的 Web API 的可用性和功能性。但是,它还需要一些仔细的考虑和最佳实践,以确保其正确实施并避免潜在的陷阱。以下是我们在 Web API 中使用内容协商时应遵循的一些最佳实践:
- 媒体类型的命名约定:我们应该使用标准且一致的媒体类型命名约定,如IANA 媒体类型注册表所定义。我们还应避免使用特定于供应商或自定义的媒体类型,除非它们有详尽的文档记录并被社区广泛接受。例如,我们应该使用
application/json而不是application/vnd.example+json。 - 错误处理和优雅降级:执行内容协商时,我们应该优雅地处理错误和异常。我们应该使用适当的 HTTP 状态代码和标头向客户端提供有意义且信息丰富的错误消息。我们还应该为客户端提供后备选项,例如默认媒体类型或替代表示,以防协商失败或服务器不支持请求的媒体类型。
- 管理性能注意事项:我们应该意识到内容协商对我们的 Web API 的性能影响。我们应该避免不必要或过度的协商,因为这可能会增加我们的 Web 服务的延迟和开销。我们还应该使用缓存技术来提高我们的 Web 服务的性能和可扩展性。
真实用例
内容协商是许多实际 Web API 场景中广泛使用的功能。它使 Web 服务能够支持多样化和动态的客户端需求和偏好,同时保持一致和连贯的 Web 服务逻辑和功能。以下是应用内容协商的一些实际用例示例:
- 电子商务 Web API:提供产品信息的电子商务 Web API 可以使用内容协商为不同的客户端返回不同格式的数据。例如,它可以为 Web 应用程序返回 JSON 格式的数据、为旧式系统返回 XML 格式的数据、为浏览器返回 HTML 格式的数据或为电子表格返回 CSV 格式的数据。它还可以使用内容协商为不同的地区或市场返回不同语言或货币的数据。
- 新闻 Web API:提供新闻文章的新闻 Web API 可以使用内容协商为不同的客户端返回不同格式和质量的数据。例如,它可以为浏览器返回 HTML 格式的数据、为订阅阅读器返回 RSS 格式的数据或为打印返回 PDF 格式的数据。它还可以使用内容协商为不同的网络条件或带宽限制返回不同分辨率或压缩级别的数据。
- 图片 Web API:提供图片处理服务的图片 Web API 可以使用内容协商为不同的客户端返回不同格式和大小的数据。例如,它可以为无损质量返回 PNG 格式的数据、为有损质量返回 JPEG 格式的数据、为动画返回 GIF 格式的数据或为矢量图形返回 SVG 格式的数据。它还可以使用内容协商为不同的显示设备或方向返回不同尺寸或宽高比的数据。
结论
内容协商是一个允许客户端和服务器就 Web 请求中交换的最佳数据格式达成一致的过程。它是 Web API 开发的一个重要功能,因为它可以创建灵活且可互操作的 Web 服务,以支持多种媒体类型。在本文中,我们探讨了内容协商的工作原理、它的重要性以及如何在 .NET 6 中实现它。我们还讨论了 Web API 中内容协商的一些最佳实践、用例和优化技术。
如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。
1万+

被折叠的 条评论
为什么被折叠?



