ASP.NET Core媒体类型:自定义格式支持
在现代Web开发中,处理多种数据格式是API设计的核心需求。ASP.NET Core提供了强大的媒体类型(Media Type)处理机制,允许开发者轻松实现自定义格式支持。本文将深入探讨ASP.NET Core中的媒体类型系统,并展示如何创建自定义输入和输出格式化器。
媒体类型基础概念
媒体类型(Media Type),也称为MIME类型(Multipurpose Internet Mail Extensions),是标识数据格式的标准方式。在HTTP协议中,通过Content-Type
和Accept
头来协商客户端和服务器之间的数据交换格式。
媒体类型结构
常见媒体类型示例:
application/json
- JSON格式数据application/xml
- XML格式数据text/plain; charset=utf-8
- 纯文本,UTF-8编码application/vnd.api+json
- 符合JSON API规范的格式
ASP.NET Core格式化器架构
ASP.NET Core使用格式化器(Formatter)来处理请求和响应的序列化与反序列化。系统内置了多种格式化器,同时也支持自定义实现。
核心接口和基类
// 输入格式化器接口
public interface IInputFormatter
{
bool CanRead(InputFormatterContext context);
Task<InputFormatterResult> ReadAsync(InputFormatterContext context);
}
// 输出格式化器接口
public interface IOutputFormatter
{
bool CanWriteResult(OutputFormatterCanWriteContext context);
Task WriteAsync(OutputFormatterWriteContext context);
}
// 抽象基类
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
格式化器选择流程
创建自定义输入格式化器
让我们创建一个处理CSV格式的自定义输入格式化器。
CSV输入格式化器实现
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
using System.Text;
public class CsvInputFormatter : TextInputFormatter
{
public CsvInputFormatter()
{
SupportedMediaTypes.Add("text/csv");
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanReadType(Type type)
{
return type.IsAssignableTo(typeof(IEnumerable<object>));
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context, Encoding encoding)
{
var httpContext = context.HttpContext;
using var reader = new StreamReader(httpContext.Request.Body, encoding);
var content = await reader.ReadToEndAsync();
var modelType = context.ModelType.GetGenericArguments()[0];
var items = ParseCsv(content, modelType);
return await InputFormatterResult.SuccessAsync(items);
}
private object ParseCsv(string csvContent, Type modelType)
{
var lines = csvContent.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length < 2) return Array.CreateInstance(modelType, 0);
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
var listType = typeof(List<>).MakeGenericType(modelType);
var list = Activator.CreateInstance(listType);
var addMethod = listType.GetMethod("Add");
for (int i = 1; i < lines.Length; i++)
{
var values = lines[i].Split(',');
var instance = Activator.CreateInstance(modelType);
for (int j = 0; j < Math.Min(headers.Length, values.Length); j++)
{
var property = modelType.GetProperty(headers[j]);
if (property != null && property.CanWrite)
{
var value = Convert.ChangeType(values[j].Trim(), property.PropertyType, CultureInfo.InvariantCulture);
property.SetValue(instance, value);
}
}
addMethod?.Invoke(list, new[] { instance });
}
return list;
}
}
创建自定义输出格式化器
现在创建一个对应的CSV输出格式化器。
CSV输出格式化器实现
using Microsoft.AspNetCore.Mvc.Formatters;
using System.Globalization;
using System.Text;
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add("text/csv");
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanWriteType(Type type)
{
return type.IsAssignableTo(typeof(IEnumerable<object>));
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context, Encoding encoding)
{
var response = context.HttpContext.Response;
var objects = context.Object as IEnumerable<object>;
if (objects == null || !objects.Any())
{
await response.WriteAsync("", encoding);
return;
}
var firstObject = objects.First();
var properties = firstObject.GetType().GetProperties();
var header = string.Join(",", properties.Select(p => p.Name));
var csvBuilder = new StringBuilder();
csvBuilder.AppendLine(header);
foreach (var obj in objects)
{
var values = properties.Select(p =>
Convert.ToString(p.GetValue(obj), CultureInfo.InvariantCulture) ?? "");
csvBuilder.AppendLine(string.Join(",", values));
}
await response.WriteAsync(csvBuilder.ToString(), encoding);
}
}
注册自定义格式化器
在Startup.cs或Program.cs中注册自定义格式化器:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
// 添加自定义输入格式化器
options.InputFormatters.Add(new CsvInputFormatter());
// 添加自定义输出格式化器
options.OutputFormatters.Add(new CsvOutputFormatter());
// 配置格式化器映射
options.FormatterMappings.SetMediaTypeMappingForFormat(
"csv", MediaTypeHeaderValue.Parse("text/csv"));
});
var app = builder.Build();
使用FormatFilter进行内容协商
ASP.NET Core提供了[FormatFilter]
特性,允许通过URL参数指定响应格式。
配置和使用FormatFilter
// 在控制器中使用
[ApiController]
[Route("api/[controller]")]
[FormatFilter]
public class ProductsController : ControllerBase
{
[HttpGet]
[Route("")]
public IEnumerable<Product> GetProducts()
{
return _productService.GetAllProducts();
}
[HttpGet]
[Route("{id}.{format?}")]
public Product GetProduct(int id)
{
return _productService.GetProductById(id);
}
}
访问示例:
/api/products
- 默认格式(通常是JSON)/api/products.csv
- CSV格式/api/products/1.json
- 单个产品的JSON格式/api/products/1.csv
- 单个产品的CSV格式
高级自定义格式场景
1. 支持多种版本格式
public class VersionedCsvOutputFormatter : TextOutputFormatter
{
public VersionedCsvOutputFormatter()
{
SupportedMediaTypes.Add("text/csv; version=1.0");
SupportedMediaTypes.Add("text/csv; version=2.0");
SupportedEncodings.Add(Encoding.UTF8);
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context, Encoding encoding)
{
var version = context.HttpContext.Request.Headers["Accept"]
.FirstOrDefault()?.Split(';')
.FirstOrDefault(p => p.Trim().StartsWith("version="))
?.Split('=')[1]?.Trim();
// 根据版本号实现不同的序列化逻辑
if (version == "2.0")
{
await WriteV2Format(context, encoding);
}
else
{
await WriteV1Format(context, encoding);
}
}
private async Task WriteV1Format(OutputFormatterWriteContext context, Encoding encoding) { /* ... */ }
private async Task WriteV2Format(OutputFormatterWriteContext context, Encoding encoding) { /* ... */ }
}
2. 基于配置的动态格式化器
public class ConfigurableOutputFormatter : IOutputFormatter
{
private readonly IConfiguration _configuration;
public ConfigurableOutputFormatter(IConfiguration configuration)
{
_configuration = configuration;
SupportedMediaTypes = new MediaTypeCollection
{
"application/vnd.configurable+json"
};
}
public MediaTypeCollection SupportedMediaTypes { get; }
public bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return context.ObjectType != null &&
SupportedMediaTypes.Any(mt =>
context.HttpContext.Request.Headers.Accept.Contains(mt));
}
public async Task WriteAsync(OutputFormatterWriteContext context)
{
var config = _configuration.GetSection("Formatting:Custom");
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = config["NamingPolicy"] == "CamelCase"
? JsonNamingPolicy.CamelCase
: null,
WriteIndented = config.GetValue<bool>("PrettyPrint")
};
await JsonSerializer.SerializeAsync(
context.HttpContext.Response.Body,
context.Object,
options);
}
}
性能优化和最佳实践
1. 格式化器缓存策略
public class CachedOutputFormatter : IOutputFormatter
{
private readonly ConcurrentDictionary<Type, byte[]> _cache = new();
public async Task WriteAsync(OutputFormatterWriteContext context)
{
var type = context.Object.GetType();
if (!_cache.TryGetValue(type, out var cachedData))
{
// 序列化并缓存
using var stream = new MemoryStream();
await SerializeToStream(context.Object, stream);
cachedData = stream.ToArray();
_cache[type] = cachedData;
}
await context.HttpContext.Response.Body.WriteAsync(cachedData);
}
private async Task SerializeToStream(object obj, Stream stream)
{
// 自定义序列化逻辑
}
}
2. 异步处理最佳实践
public class AsyncAwareInputFormatter : InputFormatter
{
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context)
{
try
{
// 使用异步流处理
await using var stream = new MemoryStream();
await context.HttpContext.Request.Body.CopyToAsync(stream);
stream.Position = 0;
// 异步解析
return await ParseAsync(stream, context.ModelType);
}
catch (Exception ex)
{
context.ModelState.AddModelError(string.Empty, $"解析失败: {ex.Message}");
return InputFormatterResult.Failure();
}
}
}
测试自定义格式化器
单元测试示例
public class CsvFormatterTests
{
[Fact]
public async Task CsvInputFormatter_CanRead_CsvContentType()
{
// Arrange
var formatter = new CsvInputFormatter();
var context = new InputFormatterContext(
new DefaultHttpContext(),
"test",
new ModelStateDictionary(),
new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<Product>)),
(stream, encoding) => new StreamReader(stream, encoding));
context.HttpContext.Request.ContentType = "text/csv";
// Act
var canRead = formatter.CanRead(context);
// Assert
Assert.True(canRead);
}
[Fact]
public async Task CsvOutputFormatter_Writes_CorrectFormat()
{
// Arrange
var formatter = new CsvOutputFormatter();
var products = new List<Product>
{
new Product { Id = 1, Name = "Product A", Price = 10.99m },
new Product { Id = 2, Name = "Product B", Price = 20.50m }
};
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = new MemoryStream();
var context = new OutputFormatterWriteContext(
httpContext,
(stream, encoding) => new StreamWriter(stream, encoding),
typeof(List<Product>),
products);
// Act
await formatter.WriteAsync(context);
httpContext.Response.Body.Position = 0;
var result = new StreamReader(httpContext.Response.Body).ReadToEnd();
// Assert
Assert.Contains("Id,Name,Price", result);
Assert.Contains("1,Product A,10.99", result);
}
}
故障排除和调试
常见问题及解决方案
问题 | 原因 | 解决方案 |
---|---|---|
格式化器不被调用 | 媒体类型不匹配 | 检查SupportedMediaTypes配置 |
序列化异常 | 类型不支持 | 实现CanReadType/CanWriteType方法 |
编码问题 | 字符集配置错误 | 设置SupportedEncodings属性 |
性能问题 | 重复序列化 | 实现缓存机制 |
调试技巧
// 添加诊断日志
public class DiagnosticOutputFormatter : IOutputFormatter
{
private readonly ILogger<DiagnosticOutputFormatter> _logger;
public DiagnosticOutputFormatter(ILogger<DiagnosticOutputFormatter> logger)
{
_logger = logger;
}
public async Task WriteAsync(OutputFormatterWriteContext context)
{
_logger.LogInformation("格式化类型: {Type}", context.ObjectType);
_logger.LogInformation("请求内容类型: {ContentType}",
context.HttpContext.Request.Headers.Accept);
// 实际格式化逻辑
}
}
总结
ASP.NET Core的媒体类型系统提供了强大的扩展能力,通过自定义格式化器可以轻松支持各种数据格式。关键要点包括:
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考