Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Content Negotiation

Overview Questions

  • 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 Basics

What is Content Negotiation?

┌─────────────────────────────────────────────────────────────────┐
│                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                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Default Configuration

// ASP.NET Core mặc định chỉ support JSON
builder.Services.AddControllers();

// JSON là default formatter
// System.Text.Json được sử dụng

Input Formatters

JSON Input (Default)

// 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);
}

XML Input

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);
}

Custom Input Formatter

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());
});

Output Formatters

JSON Output (Default)

[HttpGet]
public IActionResult Get()
{
    return Ok(new { name = "Product", price = 99.99 });
}

// Response
// Content-Type: application/json
// {"name":"Product","price":99.99}

XML Output

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>

Content Negotiation

[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

JSON Configuration

System.Text.Json Options

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());
    });

Newtonsoft.Json Options

builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = 
            new CamelCasePropertyNamesContractResolver();
        
        options.SerializerSettings.NullValueHandling = 
            NullValueHandling.Ignore;
        
        options.SerializerSettings.DateFormatString = 
            "yyyy-MM-dd HH:mm:ss";
    });

Custom Output Formatter

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());
});

Best Practices

1. Use JSON by Default

// ✅ 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();

2. Configure Consistent JSON Settings

// ✅ Configure JSON settings globally
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = 
            JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.WriteIndented = true;
    });

3. Use [Produces] Attribute

// ✅ Explicit về response format
[HttpGet]
[Produces("application/json")]
public IActionResult Get() => Ok(products);

// ❌ Không rõ response format
[HttpGet]
public IActionResult Get() => Ok(products);