Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web Api Parameter binding: snake_case to camelCase

I'm doing some work on a new web api project, and my methods/ parameters follow the standard c# naming format; the method name is PascalCase, the parameters are camelCase.

Unfortunately I just discovered that documentation to a client has been published with all the parameters being ruby/ php style, with snake_case type parameter names.

The return objects and POST object were easy to convert; I used a version of crallen's code to replace the default Json input/ output, but on GET requests, there doesn't seem to be such an easy answer.

I'd prefer to keep my naming conventions the same. Is there a way to tell the binder to automatically change my_parameter into myParameter for all requests? Do I have to build a completely different binder?

For example, if I have this as a method signature:

[Route("~/api/Widgets")]
[ResponseType(typeof(Widget))]
public async Task<HttpResponseMessage> GetWidget(int widgetId, int groupId)
{
    . . .

I would like to be able to use this in the URL

https://myserver.com/api/Widgets?widget_id=12345&group_id=54321

Do I have to reinvent the wheel to get this to work? I've seen examples of replacing specific type model binders, but nothing at this level. Am I better off just changing my parameter names in code?


2 Answers

You can achieve this by using a custom ApiControllerActionSelector that rewrites Request.RequestUri and then calls the base selector.

Here it goes:

First, create the custom selector:

public class SnakeCaseActionSelector : ApiControllerActionSelector
{
    public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        var newUri = CreateNewUri(
            controllerContext.Request.RequestUri, 
            controllerContext.Request.GetQueryNameValuePairs());

        controllerContext.Request.RequestUri = newUri;

        return base.SelectAction(controllerContext);
    }

    private Uri CreateNewUri(Uri requestUri, IEnumerable<KeyValuePair<string, string>> queryPairs)
    {
        var currentQuery = requestUri.Query;
        var newQuery = ConvertQueryToCamelCase(queryPairs);
        return new Uri(requestUri.ToString().Replace(currentQuery, newQuery));
    }

    private static string ConvertQueryToCamelCase(IEnumerable<KeyValuePair<string, string>> queryPairs)
    {
        queryPairs = queryPairs
            .Select(x => new KeyValuePair<string, string>(x.Key.ToCamelCase(), x.Value));

        return "?" + queryPairs
            .Select(x => String.Format("{0}={1}", x.Key, x.Value))
            .Aggregate((x, y) => x + "&" + y);
    }
}

Next, create some extensions to convert to camel case and to convert to capitalized words:

public static class StringExtensions
{
    public static string ToCamelCase(this string source)
    {
        var parts = source
            .Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);

        return parts
            .First().ToLower() +
            String.Join("", parts.Skip(1).Select(ToCapital));
    }

    public static string ToCapital(this string source)
    {
        return String.Format("{0}{1}", char.ToUpper(source[0]), source.Substring(1).ToLower());
    }
}

And finally add the action selector to the WebApiConfig:

config.Services.Replace(typeof(IHttpActionSelector), new SnakeCaseActionSelector());
like image 128
venerik Avatar answered Sep 05 '25 16:09

venerik


I'd like to propose an alternative solution using a custom value provider factory:

  1. Derive from QueryStringValueProviderFactory and override GetValueProvider
  2. Grab the query string pairs and modify them accordingly.
  3. Return an instance of NameValuePairsValueProvider taking in the newly modified values.
  4. Replace the original QueryStringValueProviderFactory in HttpConfiguration.Services with your own.

Roughly translates into:

public class SnakeQueryStringValueProviderFactory : QueryStringValueProviderFactory
{
    public override IValueProvider GetValueProvider( HttpActionContext actionContext )
    {
        var pairs = actionContext.ControllerContext.Request.GetQueryNameValuePairs();

        // modify query string keys accordingly
        // e.g. remove '_' from query string keys 
        // var newPairs = ...

        return new NameValuePairsValueProvider( newPairs, CultureInfo.InvariantCulture );
    }
}

The following should be improved but you get the idea.

config.Services.Remove( 
    typeof( ValueProviderFactory ),
    config.Services.GetValueProviderFactories().First( f => f is QueryStringValueProviderFactory ) 
);
config.Services.Insert( typeof( ValueProviderFactory ), 0, new SnakeQueryStringValueProviderFactory() ); // should be first
like image 38
deluvas Avatar answered Sep 05 '25 14:09

deluvas



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!