Content Negotiation là gì và tại sao cần thiết?
ASP.NET Core xử lý input/output formatting như thế nào?
Làm sao để thêm XML support?
Custom formatter được viết ra sao?
Response format negotiation hoạt động như thế nào?
┌─────────────────────────────────────────────────────────────────┐
│ CONTENT NEGOTIATION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Request: │
│ Accept: application/json │
│ Content-Type: application/json │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Input Formatter │ │
│ │ (JSON, XML, etc.) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Controller │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Output Formatter │ │
│ │ (JSON, XML, etc.) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ Client Response: │
│ Content-Type: application/json │
│ │
└─────────────────────────────────────────────────────────────────┘
// ASP.NET Core mặc định chỉ support JSON
builder.Services.AddControllers();
// JSON là default formatter
// System.Text.Json được sử dụng
// Request
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}
[HttpPost]
public IActionResult Create(Product product)
{
// product được deserialize từ JSON
return Ok(product);
}
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
builder.Services.AddControllers()
.AddXmlSerializerFormatters(); // Add XML support
// Request
// POST /api/products
// Content-Type: application/xml
// <Product><Name>Laptop</Name><Price>999.99</Price></Product>
[HttpPost]
public IActionResult Create(Product product)
{
return Ok(product);
}
public class CsvInputFormatter : InputFormatter
{
public CsvInputFormatter()
{
SupportedMediaTypes.Add("text/csv");
}
protected override bool CanReadType(Type type)
{
return type == typeof(List<Product>);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context)
{
var request = context.HttpContext.Request;
using var reader = new StreamReader(request.Body);
var csv = await reader.ReadToEndAsync();
var products = ParseCsv(csv);
return await InputFormatterResult.SuccessAsync(products);
}
private List<Product> ParseCsv(string csv)
{
// Parse CSV logic
return new List<Product>();
}
}
// Đăng ký
builder.Services.AddControllers(options =>
{
options.InputFormatters.Add(new CsvInputFormatter());
});
[HttpGet]
public IActionResult Get()
{
return Ok(new { name = "Product", price = 99.99 });
}
// Response
// Content-Type: application/json
// {"name":"Product","price":99.99}
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
[HttpGet]
[Produces("application/xml")]
public IActionResult Get()
{
return Ok(new Product { Name = "Product", Price = 99.99 });
}
// Response
// Content-Type: application/xml
// <Product><Name>Product</Name><Price>99.99</Price></Product>
[HttpGet]
public IActionResult Get()
{
var product = new Product { Name = "Product", Price = 99.99 };
return Ok(product);
}
// Request với Accept: application/json
// Response: JSON
// Request với Accept: application/xml
// Response: XML
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// CamelCase property names
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
// Ignore null values
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
// Write numbers as strings
options.JsonSerializerOptions.NumberHandling =
JsonNumberHandling.WriteAsString;
// Allow trailing commas
options.JsonSerializerOptions.AllowTrailingCommas = true;
// Custom converters
options.JsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter());
});
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.NullValueHandling =
NullValueHandling.Ignore;
options.SerializerSettings.DateFormatString =
"yyyy-MM-dd HH:mm:ss";
});
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add("text/csv");
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type type)
{
return typeof(IEnumerable<Product>).IsAssignableFrom(type);
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var products = context.Object as IEnumerable<Product>;
using var writer = new StreamWriter(response.Body, selectedEncoding);
// Write header
writer.WriteLine("Id,Name,Price");
// Write data
foreach (var product in products ?? Enumerable.Empty<Product>())
{
writer.WriteLine($"{product.Id},{product.Name},{product.Price}");
}
}
}
// Đăng ký
builder.Services.AddControllers(options =>
{
options.OutputFormatters.Add(new CsvOutputFormatter());
});
// ✅ JSON là default và được support tốt nhất
builder.Services.AddControllers();
// ❌ Tránh thêm quá nhiều formatters không cần thiết
builder.Services.AddControllers()
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
// ✅ Configure JSON settings globally
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.WriteIndented = true;
});
// ✅ Explicit về response format
[HttpGet]
[Produces("application/json")]
public IActionResult Get() => Ok(products);
// ❌ Không rõ response format
[HttpGet]
public IActionResult Get() => Ok(products);