microsoft/migrating-mvc-content-negotiation
> Migrates ASP.NET Web API content negotiation and formatters to ASP.NET Core equivalents. Converts MediaTypeFormatter subclasses to InputFormatter/OutputFormatter, replaces IContentNegotiator with OutputFormatterSelector, and updates formatter registration from HttpConfiguration.Formatters to MvcOptions. Use when upgrading Web API projects that define custom MediaTypeFormatter classes, configure content negotiation via IContentNegotiator, register XML or JSON formatters, use MediaTypeMapping, or need to migrate from Newtonsoft.Json to System.Text.Json. Also triggers for conneg migration, formatter pipeline changes, and 406 Not Acceptable behavior configuration.
npx skills add https://github.com/microsoft/upgrade-agent-plugins --skill migrating-mvc-content-negotiation
Migrate Web API content negotiation infrastructure from ASP.NET Framework to ASP.NET Core. The formatter base classes, registration model, and default serializer all changed — custom MediaTypeFormatter subclasses must be rewritten against new base classes, and projects relying on XML or Newtonsoft.Json defaults need explicit opt-in.
Track progress across these steps:
Migration Progress:
- [ ] Step 1: Inventory formatters and negotiation code
- [ ] Step 2: Configure built-in formatters
- [ ] Step 3: Migrate custom formatters
- [ ] Step 4: Migrate serialization settings
- [ ] Step 5: Update formatter registration
- [ ] Step 6: Remove legacy references
Search the codebase for content negotiation surface area:
MediaTypeFormatter or BufferedMediaTypeFormatterIContentNegotiator implementationsconfig.Formatters registrations (e.g., config.Formatters.Add(), config.Formatters.Remove())MediaTypeMapping subclasses (QueryStringMapping, UriPathExtensionMapping, RequestHeaderMapping)GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettingsJsonMediaTypeFormatter or XmlMediaTypeFormatterRecord each item and its file location before proceeding.
ASP.NET Core includes only JSON formatting by default. Opt in to additional built-in formatters as needed.
XML support — add explicitly if the project used XML content negotiation:
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
Use AddXmlDataContractSerializerFormatters() instead if the legacy project used DataContractSerializer.
406 Not Acceptable — Core returns JSON for unknown Accept headers by default. To restore strict negotiation behavior:
builder.Services.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
});
Browser Accept header — Core respects */* from browsers and returns JSON. To match legacy behavior that returned XML for browsers, add RespectBrowserAcceptHeader = true to MvcOptions.
Convert each MediaTypeFormatter subclass to its ASP.NET Core equivalent. The mapping depends on whether the formatter handles output, input, or both.
| ASP.NET Web API | ASP.NET Core |
|---|---|
| MediaTypeFormatter (output) | TextOutputFormatter or OutputFormatter |
| MediaTypeFormatter (input) | TextInputFormatter or InputFormatter |
| BufferedMediaTypeFormatter | InputFormatter / OutputFormatter (no buffered base) |
| MediaTypeMapping | Set SupportedMediaTypes on the formatter directly |
| IContentNegotiator | OutputFormatterSelector (rarely needed) |
public class CsvFormatter : BufferedMediaTypeFormatter
{
public CsvFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
public override bool CanReadType(Type type) => false;
public override bool CanWriteType(Type type) => typeof(IEnumerable).IsAssignableFrom(type);
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
using var writer = new StreamWriter(writeStream);
foreach (var item in (IEnumerable)value)
{
writer.WriteLine(FormatCsvRow(item));
}
}
}
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv"));
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type? type) =>
typeof(IEnumerable).IsAssignableFrom(type);
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
foreach (var item in (IEnumerable)context.Object!)
{
await response.WriteAsync(FormatCsvRow(item), selectedEncoding);
}
}
}
Key differences in the conversion:
BufferedMediaTypeFormatter → TextOutputFormatter. Use OutputFormatter for binary formats.WriteToStream → async WriteResponseBodyAsync with OutputFormatterWriteContext.ReadFromStream → async ReadRequestBodyAsync with InputFormatterContext.SupportedEncodings API is similar, but UTF8Encoding constructor form changes to Encoding.UTF8.MediaTypeMapping subclasses. Add media types directly to SupportedMediaTypes.The default JSON serializer changed from Newtonsoft.Json to System.Text.Json. Evaluate which approach fits the project.
| Behavior | Newtonsoft.Json (Web API default) | System.Text.Json (Core default) |
|---|---|---|
| Property naming | camelCase via CamelCasePropertyNamesContractResolver | camelCase via JsonNamingPolicy.CamelCase |
| Null handling | Includes nulls | Includes nulls (DefaultIgnoreCondition = Never) |
| Case-insensitive read | Off by default | On by default (PropertyNameCaseInsensitive = true) |
| Circular references | Handled via PreserveReferencesHandling | ReferenceHandler.Preserve (opt-in) |
| Missing members | Ignored | Ignored |
| Comments in JSON | Allowed | Rejected (opt-in via ReadCommentHandling) |
| Trailing commas | Allowed | Rejected (opt-in via AllowTrailingCommas) |
| Number from string | Allowed | Rejected (opt-in via NumberHandling) |
| Attribute for property name | [JsonProperty("name")] | [JsonPropertyName("name")] |
| Custom converter base | JsonConverter | JsonConverter<T> |
Configure System.Text.Json to match prior behavior where needed:
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
options.JsonSerializerOptions.AllowTrailingCommas = true;
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
});
Replace Newtonsoft attributes on model classes:
// Before
[JsonProperty("user_name")]
public string UserName { get; set; }
// After
[JsonPropertyName("user_name")]
public string UserName { get; set; }
Migrate custom JsonConverter implementations to JsonConverter<T>:
// Before (Newtonsoft)
public class DateOnlyConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer) { /* ... */ }
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer) { /* ... */ }
}
// After (System.Text.Json)
public class DateOnlyConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options) { /* ... */ }
public override void Write(Utf8JsonWriter writer, DateOnly value,
JsonSerializerOptions options) { /* ... */ }
}
Install Microsoft.AspNetCore.Mvc.NewtonsoftJson and opt in:
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
// Carry over existing serializer settings
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
Choose this option when the project has many custom JsonConverter classes, relies on JObject/JToken extensively, or when API clients depend on exact Newtonsoft serialization behavior.
Replace Web API formatter registration with ASP.NET Core equivalents.
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.Add(new CsvFormatter());
config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
builder.Services.AddControllers(options =>
{
options.OutputFormatters.Add(new CsvOutputFormatter());
options.InputFormatters.Add(new CsvInputFormatter());
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
Registration mapping:
| Web API | ASP.NET Core |
|---|---|
| config.Formatters.Add(formatter) | options.OutputFormatters.Add(formatter) / options.InputFormatters.Add(formatter) |
| config.Formatters.Remove(formatter) | options.OutputFormatters.RemoveType<T>() / options.InputFormatters.RemoveType<T>() |
| config.Formatters.Insert(0, formatter) | options.OutputFormatters.Insert(0, formatter) |
| config.Formatters.JsonFormatter | AddJsonOptions() or AddNewtonsoftJson() |
| config.Formatters.XmlFormatter | AddXmlSerializerFormatters() |
Remove these legacy types and namespaces after migration:
System.Net.Http.Formatting namespace and NuGet packageMediaTypeFormatter, BufferedMediaTypeFormatter base classesIContentNegotiator implementationsMediaTypeMapping subclasses (QueryStringMapping, UriPathExtensionMapping, RequestHeaderMapping)GlobalConfiguration.Configuration.Formatters referencesJsonMediaTypeFormatter and XmlMediaTypeFormatter direct referencesVerify the project builds without errors and that API responses return correct content types by testing with Accept: application/json, Accept: application/xml, and any custom media types.
MediaTypeFormatter subclasses converted to OutputFormatter/InputFormatterAddJsonOptions() or AddNewtonsoftJson()config.Formatters to MvcOptionsSystem.Net.Http.Formatting, IContentNegotiator, or MediaTypeMappingTake microsoft/migrating-mvc-content-negotiation from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.