diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsExtensions.cs
new file mode 100644
index 0000000000..876325d281
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsExtensions.cs
@@ -0,0 +1,88 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Formatters.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class MvcJsonOptionsExtensions
+ {
+ ///
+ /// Configures the casing behavior of JSON serialization to use camel case for property names,
+ /// and optionally for dynamic types and dictionary keys.
+ ///
+ ///
+ /// This method modifies .
+ ///
+ ///
+ /// If true will camel case dictionary keys and properties of dynamic objects.
+ /// with camel case settings.
+ public static MvcJsonOptions UseCamelCasing(this MvcJsonOptions options, bool processDictionaryKeys)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (options.SerializerSettings.ContractResolver is DefaultContractResolver resolver)
+ {
+ resolver.NamingStrategy = new CamelCaseNamingStrategy
+ {
+ ProcessDictionaryKeys = processDictionaryKeys
+ };
+ }
+ else
+ {
+ if (options.SerializerSettings.ContractResolver == null)
+ {
+ throw new InvalidOperationException(Resources.FormatContractResolverCannotBeNull(nameof(JsonSerializerSettings.ContractResolver)));
+ }
+
+ var contractResolverName = options.SerializerSettings.ContractResolver.GetType().Name;
+ throw new InvalidOperationException(
+ Resources.FormatInvalidContractResolverForJsonCasingConfiguration(contractResolverName, nameof(DefaultContractResolver)));
+ }
+
+ return options;
+ }
+
+ ///
+ /// Configures the casing behavior of JSON serialization to use the member's casing for property names,
+ /// properties of dynamic types, and dictionary keys.
+ ///
+ ///
+ /// This method modifies .
+ ///
+ ///
+ /// with member casing settings.
+ public static MvcJsonOptions UseMemberCasing(this MvcJsonOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (options.SerializerSettings.ContractResolver is DefaultContractResolver resolver)
+ {
+ resolver.NamingStrategy = new DefaultNamingStrategy();
+ }
+ else
+ {
+ if (options.SerializerSettings.ContractResolver == null)
+ {
+ throw new InvalidOperationException(Resources.FormatContractResolverCannotBeNull(nameof(JsonSerializerSettings.ContractResolver)));
+ }
+
+ var contractResolverName = options.SerializerSettings.ContractResolver.GetType().Name;
+ throw new InvalidOperationException(
+ Resources.FormatInvalidContractResolverForJsonCasingConfiguration(contractResolverName, nameof(DefaultContractResolver)));
+ }
+
+ return options;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7982959233
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Formatters.Json.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..b552db2345
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+//
+namespace Microsoft.AspNetCore.Mvc.Formatters.Json
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Mvc.Formatters.Json.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// {0} cannot be null.
+ ///
+ internal static string ContractResolverCannotBeNull
+ {
+ get => GetString("ContractResolverCannotBeNull");
+ }
+
+ ///
+ /// {0} cannot be null.
+ ///
+ internal static string FormatContractResolverCannotBeNull(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ContractResolverCannotBeNull"), p0);
+
+ ///
+ /// Cannot configure JSON casing behavior on '{0}' contract resolver. The supported contract resolver is {1}.
+ ///
+ internal static string InvalidContractResolverForJsonCasingConfiguration
+ {
+ get => GetString("InvalidContractResolverForJsonCasingConfiguration");
+ }
+
+ ///
+ /// Cannot configure JSON casing behavior on '{0}' contract resolver. The supported contract resolver is {1}.
+ ///
+ internal static string FormatInvalidContractResolverForJsonCasingConfiguration(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("InvalidContractResolverForJsonCasingConfiguration"), p0, p1);
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Resources.resx
new file mode 100644
index 0000000000..729545d687
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Resources.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ {0} cannot be null.
+
+
+ Cannot configure JSON casing behavior on '{0}' contract resolver. The supported contract resolver is {1}.
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/MvcJsonOptionsExtensionsTests.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/MvcJsonOptionsExtensionsTests.cs
new file mode 100644
index 0000000000..b3370c312a
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/MvcJsonOptionsExtensionsTests.cs
@@ -0,0 +1,264 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Formatters.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using Xunit;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public class MvcJsonOptionsExtensionsTests
+ {
+ [Fact]
+ public void UseCamelCasing_WillSet_CamelCasingStrategy_NameStrategy()
+ {
+ // Arrange
+ var options = new MvcJsonOptions();
+ options.SerializerSettings.ContractResolver = new DefaultContractResolver()
+ {
+ NamingStrategy = new DefaultNamingStrategy()
+ };
+ var expected = typeof(CamelCaseNamingStrategy);
+
+ // Act
+ options.UseCamelCasing(processDictionaryKeys: true);
+ var resolver = options.SerializerSettings.ContractResolver as DefaultContractResolver;
+ var actual = resolver.NamingStrategy;
+
+ // Assert
+ Assert.IsType(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_WillNot_OverrideSpecifiedNames()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: true);
+ var annotatedFoo = new AnnotatedFoo()
+ {
+ HelloWorld = "Hello"
+ };
+ var expected = "{\"HELLO-WORLD\":\"Hello\"}";
+
+ // Act
+ var actual = SerializeToJson(options, value: annotatedFoo);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_WillChange_PropertyNames()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: true);
+ var foo = new { TestName = "TestFoo", TestValue = 10 };
+ var expected = "{\"testName\":\"TestFoo\",\"testValue\":10}";
+
+ // Act
+ var actual = SerializeToJson(options, value: foo);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_WillChangeFirstPartBeforeSeparator_InPropertyName()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: true);
+ var foo = new { TestFoo_TestValue = "Test" };
+ var expected = "{\"testFoo_TestValue\":\"Test\"}";
+
+ // Act
+ var actual = SerializeToJson(options, value: foo);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_ProcessDictionaryKeys_WillChange_DictionaryKeys_IfTrue()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: true);
+ var dictionary = new Dictionary
+ {
+ ["HelloWorld"] = 1,
+ ["HELLOWORLD"] = 2
+ };
+ var expected = "{\"helloWorld\":1,\"helloworld\":2}";
+
+ // Act
+ var actual = SerializeToJson(options, value: dictionary);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_ProcessDictionaryKeys_WillChangeFirstPartBeforeSeparator_InDictionaryKey_IfTrue()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: true);
+ var dictionary = new Dictionary()
+ {
+ ["HelloWorld_HelloWorld"] = 1
+ };
+
+ var expected = "{\"helloWorld_HelloWorld\":1}";
+
+ // Act
+ var actual = SerializeToJson(options, value: dictionary);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_ProcessDictionaryKeys_WillNotChangeDictionaryKeys_IfFalse()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseCamelCasing(processDictionaryKeys: false);
+ var dictionary = new Dictionary
+ {
+ ["HelloWorld"] = 1,
+ ["HELLO-WORLD"] = 2
+ };
+ var expected = "{\"HelloWorld\":1,\"HELLO-WORLD\":2}";
+
+ // Act
+ var actual = SerializeToJson(options, value: dictionary);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseMemberCasing_WillNotChange_OverrideSpecifiedNames()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseMemberCasing();
+ var annotatedFoo = new AnnotatedFoo()
+ {
+ HelloWorld = "Hello"
+ };
+ var expected = "{\"HELLO-WORLD\":\"Hello\"}";
+
+ // Act
+ var actual = SerializeToJson(options, value: annotatedFoo);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseMemberCasing_WillSet_DefaultNamingStrategy_AsNamingStrategy()
+ {
+ // Arrange
+ var options = new MvcJsonOptions();
+ options.SerializerSettings.ContractResolver = new DefaultContractResolver
+ {
+ NamingStrategy = new CamelCaseNamingStrategy()
+ };
+ var expected = typeof(DefaultNamingStrategy);
+
+ // Act
+ options.UseMemberCasing();
+ var resolver = options.SerializerSettings.ContractResolver as DefaultContractResolver;
+ var actual = resolver.NamingStrategy;
+
+ // Assert
+ Assert.IsType(expected, actual);
+ }
+
+ [Fact]
+ public void UseMemberCasing_WillNotChange_PropertyNames()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseMemberCasing();
+ var foo = new { fooName = "Test", FooValue = "Value"};
+ var expected = "{\"fooName\":\"Test\",\"FooValue\":\"Value\"}";
+
+ // Act
+ var actual = SerializeToJson(options, value: foo);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseMemberCasing_WillNotChange_DictionaryKeys()
+ {
+ // Arrange
+ var options = new MvcJsonOptions().UseMemberCasing();
+ var dictionary = new Dictionary()
+ {
+ ["HelloWorld"] = 1,
+ ["helloWorld"] = 2,
+ ["HELLO-WORLD"] = 3
+ };
+ var expected = "{\"HelloWorld\":1,\"helloWorld\":2,\"HELLO-WORLD\":3}";
+
+ // Act
+ var actual = SerializeToJson(options, value: dictionary);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void UseCamelCasing_WillThrow_IfContractResolver_IsNot_DefaultContractResolver()
+ {
+ // Arrange
+ var options = new MvcJsonOptions();
+ options.SerializerSettings.ContractResolver = new FooContractResolver();
+ var expectedMessage = Resources.FormatInvalidContractResolverForJsonCasingConfiguration(nameof(FooContractResolver), nameof(DefaultContractResolver));
+
+ // Act & Assert
+ var exception = Assert.Throws(
+ () => options.UseCamelCasing(processDictionaryKeys: false));
+ Assert.Equal(expectedMessage, actual: exception.Message);
+ }
+
+ [Fact]
+ public void UseMemberCasing_WillThrow_IfContractResolver_IsNot_DefaultContractResolver()
+ {
+ // Arrange
+ var options = new MvcJsonOptions();
+ options.SerializerSettings.ContractResolver = new FooContractResolver();
+ var expectedMessage = Resources.FormatInvalidContractResolverForJsonCasingConfiguration(nameof(FooContractResolver), nameof(DefaultContractResolver));
+
+ // Act & Assert
+ var exception = Assert.Throws(
+ () => options.UseMemberCasing());
+ Assert.Equal(expectedMessage, actual: exception.Message);
+ }
+
+ private static string SerializeToJson(MvcJsonOptions options, object value)
+ {
+ return JsonConvert.SerializeObject(
+ value: value,
+ formatting: Formatting.None,
+ settings: options.SerializerSettings);
+ }
+
+ private class AnnotatedFoo
+ {
+ [JsonProperty("HELLO-WORLD")]
+ public string HelloWorld { get; set; }
+ }
+
+ private class FooContractResolver : IContractResolver
+ {
+ public JsonContract ResolveContract(Type type)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}