1

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

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Is your issue with [tag:asp.net-web-api] or [tag:asp.net-mvc-5]? They use different serializers, and the serializer used changes with the version, so might you please [edit] your question to be specific as to exactly which version and setup you are using? – dbc Mar 31 '21 at 17:47
  • If MVC, be aware that [tag:asp.net-mvc-5] is pretty old, the first version was released in 2013. It doesn't use Json.NET by default. If you want to use Json.NET for serialization in MVC5, see [Setting the default JSON serializer in ASP.NET MVC](https://stackoverflow.com/q/14591750/3744182/). If you want to use it for model binding, look at [How to use Json.NET for JSON modelbinding in an MVC5 project?](https://stackoverflow.com/q/23995210/3744182/). – dbc Mar 31 '21 at 17:50
  • i'm using both, asp net mvc 5 and asp net web api, .net framework 4.8 – Matheus Vieira Mar 31 '21 at 18:43

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340