Search code examples
asp.netjsonasp.net-web-apiasp.net-mvc-5json.net

Cannot set a custom contract resolver in web api configuration


How can I set a custom contract resolver in web api configuration? My code is relatively new and has no custom contract resolver till now.

I have added no other customization besides routing.

I tried in three different ways and none worked:

public static void Register(HttpConfiguration config)
{            
    // Web API routes
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{action}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    //attempt 1
    config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CustomContractResolver();

    //attempt 2
    GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CustomContractResolver();

    //attempt 3
    JsonConvert.DefaultSettings = () => new JsonSerializerSettings
    {
        ContractResolver = new CustomContractResolver()
    };           
}

The custom contract resolver code, breakpoint never reaches here when I'm debugging:

public class CustomContractResolver : CamelCasePropertyNamesContractResolver
{
    protected override string ResolvePropertyName(string propertyName)
    {
        var regex = new Regex(@"([_])(\w)");

        if (regex.IsMatch(propertyName))
        {
            var result = regex.Replace(propertyName.ToLower(), (match) => { return match.Groups[2].Value.ToUpper(); });
            return result;
        }
        else
            return base.ResolvePropertyName(propertyName);
    }
}

Is there something that is missing?

Edit 1:

I'm using ASP.NET WebApi 5.2.1 AND MVC 5.2.7, JSON.NET (Newtonsoft.Json) v13.0.1 (and already tried the old v12)

My Global Asax is very simple as well:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        GlobalConfiguration.Configure(WebApiConfig.Register); //<- web api configuration
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes); //<- mvc configuration
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

The MVC RouteConfig class:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Edit 2

Here is some test web api controllers:

using System.Web.Http;

namespace Kronos.Web.Geolocalizacao.Controllers.Api
{
    public class TestController : ApiController
    {
        [HttpGet]
        public TestModel Obtain()
        {
            return new TestModel { CODE_IDENTIFICATION = 1, DEFAULT_DESCRIPTION = "TEST DAT THING" };
        }
    }

    public class TestModel
    {
        public decimal CODE_IDENTIFICATION { get; set; }

        public string DEFAULT_DESCRIPTION { get; set; }
    }
}

Used the Tabbed Postman chrome addon to test

Postman tests


Solution

  • Your problem has nothing to do with how you are registering your global settings -- setting config.Formatters.JsonFormatter.SerializerSettings.ContractResolver is correct as per this question. Your problem is that Json.NET does not call ResolvePropertyName() when the contract resolver also has a NamingStrategy -- and your base class CamelCasePropertyNamesContractResolver does indeed have a naming strategy.

    This can be verified by checking the current Json.NET reference source for DefaultContractResolver.SetPropertySettingsFromAttributes():

    if (namingStrategy != null)
    {
        property.PropertyName = namingStrategy.GetPropertyName(mappedName, hasSpecifiedName);
    }
    else
    {
        property.PropertyName = ResolvePropertyName(mappedName);
    }
    

    Broken demo fiddle #1 here.

    If I simply modify your CustomContractResolver to inherit from DefaultContractResolver (which has a null NamingStrategy by default), then it works:

    public class CustomContractResolver : DefaultContractResolver
    {
        readonly NamingStrategy baseNamingStrategy = new CamelCaseNamingStrategy();
    
        protected override string ResolvePropertyName(string propertyName)
        {
            var regex = new Regex(@"([_])(\w)");
    
            if (regex.IsMatch(propertyName))
            {
                var result = regex.Replace(propertyName.ToLower(), (match) => { return match.Groups[2].Value.ToUpper(); });
                return result;
            }
            else
                return baseNamingStrategy.GetPropertyName(propertyName, false);
        }
    }
    

    Fixed demo fiddle #2 here.

    However, a cleaner solution would be to replace your custom contract resolver with a custom naming strategy:

    public class CustomNamingStrategy : CamelCaseNamingStrategy
    {
        public CustomNamingStrategy() : base() { }
        public CustomNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames) : base(processDictionaryKeys, overrideSpecifiedNames) { }
        public CustomNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) : base(processDictionaryKeys, overrideSpecifiedNames, processExtensionDataNames) { }
    
        readonly Regex regex = new Regex(@"([_])(\w)");
        protected  override string ResolvePropertyName(string name)
        {
            if (regex.IsMatch(name))
            {
                var result = regex.Replace(name.ToLower(), (match) => { return match.Groups[2].Value.ToUpper(); });
                return result;
            }
            return base.ResolvePropertyName(name);
        }
    }
    

    And then configure it in settings like so:

    settings.ContractResolver = new DefaultContractResolver
    {
        // Set the constructor parameters as per your preference.  These values are consistent with CamelCasePropertyNamesContractResolver
        NamingStrategy = new CustomNamingStrategy(processDictionaryKeys: true, overrideSpecifiedNames: true),
    };
    

    Demo fiddle #3 here.