C#中的内容协商机制是Web API框架根据客户端请求头中的Accept等信息,自动选择最合适的响应数据格式返回给客户端的功能,默认支持JSON和XML等常见格式,进阶场景下可以通过自定义扩展实现更多个性化需求。

C#内容协商机制的基本原理
内容协商的核心逻辑是服务器匹配客户端请求的媒体类型与自身支持的格式化器,默认情况下C# Web API会注册JsonMediaTypeFormatter和XmlMediaTypeFormatter两种格式化器,当客户端发送Accept: application/json请求时,框架会优先选择JSON格式化器序列化数据。
我们可以通过以下代码查看当前Web API项目默认注册的格式化器:
using System.Collections.Generic;
using System.Web.Http;
using System.Web.Http.Controllers;
public class FormatterController : ApiController
{
public List<string> GetFormatters()
{
// 获取当前配置的所有媒体类型格式化器
var formatters = this.Configuration.Formatters;
List<string> formatterNames = new List<string>();
foreach (var formatter in formatters)
{
formatterNames.Add(formatter.GetType().Name);
}
return formatterNames;
}
}
进阶功能一:自定义媒体类型格式化器
如果我们需要支持比如text/csv这类自定义格式,就需要自己实现媒体类型格式化器,核心是实现MediaTypeFormatter基类的相关方法。
实现自定义CSV格式化器
下面的代码实现了一个简单的CSV格式化器,用于将集合类型的数据序列化为CSV格式返回:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
public class CsvMediaTypeFormatter : MediaTypeFormatter
{
public CsvMediaTypeFormatter()
{
// 添加支持的媒体类型
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
// 设置默认字符集
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
public override bool CanWriteType(Type type)
{
// 支持所有可枚举的类型
return typeof(IEnumerable<>).IsAssignableFrom(type) || type.GetInterface(typeof(IEnumerable<>).FullName) != null;
}
public override bool CanReadType(Type type)
{
// 暂不实现读取功能
return false;
}
public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, System.Net.TransportContext transportContext)
{
var encoding = SelectCharacterEncoding(content.Headers);
using (var writer = new StreamWriter(writeStream, encoding))
{
var enumerable = value as IEnumerable<object>;
if (enumerable == null) return;
// 写入表头:获取第一个元素的属性名
var firstItem = new List<object>(enumerable).GetEnumerator();
if (firstItem.MoveNext() && firstItem.Current != null)
{
var properties = firstItem.Current.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
List<string> headers = new List<string>();
foreach (var prop in properties)
{
headers.Add(prop.Name);
}
await writer.WriteLineAsync(string.Join(",", headers));
// 写入第一个元素的值
List<string> firstValues = new List<string>();
foreach (var prop in properties)
{
var val = prop.GetValue(firstItem.Current);
firstValues.Add(val == null ? "" : val.ToString());
}
await writer.WriteLineAsync(string.Join(",", firstValues));
// 写入剩余元素的值
while (firstItem.MoveNext())
{
if (firstItem.Current == null) continue;
List<string> values = new List<string>();
foreach (var prop in properties)
{
var val = prop.GetValue(firstItem.Current);
values.Add(val == null ? "" : val.ToString());
}
await writer.WriteLineAsync(string.Join(",", values));
}
}
}
}
}
实现完成后需要将自定义格式化器注册到Web API的配置中:
using System.Web.Http;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// 注册自定义CSV格式化器
config.Formatters.Add(new CsvMediaTypeFormatter());
// 其他路由配置
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
进阶功能二:修改默认内容协商逻辑
默认的内容协商逻辑是按照格式化器注册顺序匹配,我们可以通过实现IContentNegotiator接口自定义协商规则,比如优先匹配客户端请求的特定参数而不是Accept头。
下面的代码实现了一个自定义内容协商器,优先根据请求中的format参数选择格式:
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
public class CustomContentNegotiator : IContentNegotiator
{
private readonly IContentNegotiator _defaultNegotiator;
public CustomContentNegotiator(IContentNegotiator defaultNegotiator)
{
_defaultNegotiator = defaultNegotiator;
}
public ContentNegotiationResult Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
{
// 优先检查请求中的format参数
var formatParam = request.GetQueryNameValuePairs();
string format = null;
foreach (var pair in formatParam)
{
if (pair.Key == "format")
{
format = pair.Value;
break;
}
}
if (!string.IsNullOrEmpty(format))
{
// 根据format参数匹配格式化器
MediaTypeFormatter matchedFormatter = null;
MediaTypeHeaderValue matchedMediaType = null;
foreach (var formatter in formatters)
{
if (format == "json" && formatter is JsonMediaTypeFormatter)
{
matchedFormatter = formatter;
matchedMediaType = new MediaTypeHeaderValue("application/json");
break;
}
if (format == "xml" && formatter is XmlMediaTypeFormatter)
{
matchedFormatter = formatter;
matchedMediaType = new MediaTypeHeaderValue("application/xml");
break;
}
if (format == "csv" && formatter is CsvMediaTypeFormatter)
{
matchedFormatter = formatter;
matchedMediaType = new MediaTypeHeaderValue("text/csv");
break;
}
}
if (matchedFormatter != null)
{
return new ContentNegotiationResult(matchedFormatter, matchedMediaType);
}
}
// 没有匹配到format参数时走默认协商逻辑
return _defaultNegotiator.Negotiate(type, request, formatters);
}
}
注册自定义协商器到配置中:
using System.Web.Http;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// 注册自定义内容协商器
var defaultNegotiator = config.Services.GetService(typeof(IContentNegotiator)) as IContentNegotiator;
config.Services.Replace(typeof(IContentNegotiator), new CustomContentNegotiator(defaultNegotiator));
// 注册格式化器
config.Formatters.Add(new CsvMediaTypeFormatter());
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
进阶功能三:处理特殊客户端请求场景
有些老旧客户端可能不会正确设置Accept头,我们可以通过全局配置设置默认的响应格式,比如默认返回JSON格式:
using System.Net.Http.Formatting;
using System.Web.Http;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// 移除XML格式化器,默认返回JSON
config.Formatters.Remove(config.Formatters.XmlFormatter);
// 或者调整格式化器顺序,把JSON格式化器放在第一个
// var jsonFormatter = config.Formatters.JsonFormatter;
// config.Formatters.Remove(jsonFormatter);
// config.Formatters.Insert(0, jsonFormatter);
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
还可以针对单个接口自定义内容协商行为,比如在接口方法中使用HttpResponseMessage直接指定响应格式:
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http;
public class UserController : ApiController
{
public HttpResponseMessage GetUsers()
{
var users = new List<object>
{
new { Id = 1, Name = "张三", Age = 20 },
new { Id = 2, Name = "李四", Age = 22 }
};
// 强制返回XML格式,忽略内容协商
return Request.CreateResponse(HttpStatusCode.OK, users, new XmlMediaTypeFormatter());
}
}
注意事项
- 自定义格式化器时需要注意处理不同类型的数据,避免出现序列化异常
- 修改默认内容协商逻辑后需要做好测试,确保不影响现有接口的正常运行
- 自定义媒体类型的名称需要符合MIME类型规范,避免客户端无法识别
- 如果项目中同时使用ASP.NET Core,内容协商的实现方式和传统Web API有所不同,需要参考对应版本的文档
ContentNegotiationC#_Web_API内容协商媒体类型修改时间:2026-06-10 21:18:47