Search code examples
asp.net-mvcasp.net-web-api2odataaspnet-api-versioning

Change casing of JSON properties between api versions using Microsoft's ASP.NET API Versioning for Web API 2 and ODATA?


I am introducing API versioning to an existing API. The existing JSON uses Pascal casing for its property names e.g. "FooBar": "foo". For v2 of the API, I would like to use the common camel casing, "fooBar": "foo". I need to keep v1 Pascal casing so that it does not impact any client that is already pulling that version of the API.

My project is

  • ASP.NET MVC 5.2.7
  • ASP.NET WEB API 5.2.7
  • ASP.NET ODATA 7.4.0
  • ASP.NET WEB API Versioning 4.0.0

My configuration is as follows

public static class WebApiConfig
{
    public static void Register(HttpConfiguration configuration)
    {
        configuration.AddApiVersioning(options => options.ReportApiVersions = true);

        var modelBuilder = new VersionedODataModelBuilder(configuration);

        AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(x => x.GetTypes())
            .Where(x => typeof(IModelConfiguration).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
            .ForEach(t => modelBuilder.ModelConfigurations.Add((IModelConfiguration)Activator.CreateInstance(t)));

        var models = modelBuilder.GetEdmModels();

        configuration.MapVersionedODataRoutes("odata-bypath", "api/v{apiVersion}", models, builder =>
        {
            builder.AddService<IODataPathHandler>(Singleton, sp => new DefaultODataPathHandler { UrlKeyDelimiter = Parentheses });
            builder.AddService<ODataUriResolver>(Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver { EnableCaseInsensitive = true });
        });

        configuration.Count().Filter().OrderBy().Expand().Select().MaxTop(null);

        configuration.MapHttpAttributeRoutes();
    }
}

After reading through the docs and specifically Versioned ODataModelBuilder I have not found a way to change the casing based on which version of the API the model is being built for. I can get it to be all Pascal casing or all camel casing, but not v1 Pascal casing and v2 camel casing.

Adjusting the above configuration

var modelBuilder = new VersionedODataModelBuilder( configuration )
{
    ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase()
};

would use camel casing (yes I know the explicit call is unnecessary since it is the default). Then I built my own extension method ODataConventialModelBuilder().EnablePascalCase() that mimicked EnableLowerCamelCase() method to get Pascal casing to work.

var modelBuilder = new VersionedODataModelBuilder( configuration )
{
    ModelBuilderFactory = () => new ODataConventionModelBuilder().EnablePascalCase()
};

However, I could never find a way to know which version of the API I was building the model for.

At one point, I thought I had it using OnModelCreating to add

((ODataConventionModelBuilder) builder).OnModelCreating += new PascalCaser().ApplyCase;

to each of the v1 IModelConfiguration classes, but it didn't work once I was building multiple models.

Is there a way to change the JSON property naming based on which API version the model is for?


Solution

  • Using the OData Model Configurations approach described here first add a class that derives from IModelConfiguration to your project.

    Something like this:

    public class VersionedModelConfiguration : IModelConfiguration
    {
        private void ConfigureV1(ODataModelBuilder builder)
        {
            builder.EntitySet<Product>("Products");
        }
    
        private void ConfigureV2(ODataModelBuilder builder)
        {
            if (builder.GetType().Equals(typeof(ODataConventionModelBuilder)))
            {
                ((ODataConventionModelBuilder)builder).EnableLowerCamelCase();
            }
            builder.EntitySet<Product>("Products");
        }
    
        public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
        {
    
            switch (apiVersion.MajorVersion)
            {
                case 1:
                    ConfigureV1(builder);
                    break;
                case 2:
                    ConfigureV2(builder);
                    break;
                default:
                    ConfigureV1(builder);
                    break;
            }
        }
    }
    

    Then in Register method:

    // ...
    var modelBuilder = new VersionedODataModelBuilder(configuration)
    {
        ModelBuilderFactory = () => new ODataConventionModelBuilder(),
        ModelConfigurations = { new VersionedModelConfiguration() }
    };
    
    var models = modelBuilder.GetEdmModels();
    // ...
    

    Don't be tempted to leave out the line ModelBuilderFactory = () => new ODataConventionModelBuilder()

    /api/v1/$metadata:

    <?xml version="1.0" encoding="UTF-8"?>
    <edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
       <edmx:DataServices>
          <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="NS.Models">
             <EntityType Name="Product">
                <Key>
                   <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Name" Type="Edm.String" />
             </EntityType>
          </Schema>
          <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default">
             <EntityContainer Name="Container">
                <EntitySet Name="Products" EntityType="NS.Models.Product" />
             </EntityContainer>
          </Schema>
       </edmx:DataServices>
    </edmx:Edmx>
    

    /api/v2/$metadata:

    <?xml version="1.0" encoding="UTF-8"?>
    <edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
       <edmx:DataServices>
          <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="NS.Models">
             <EntityType Name="Product">
                <Key>
                   <PropertyRef Name="id" />
                </Key>
                <Property Name="id" Type="Edm.Int32" Nullable="false" />
                <Property Name="name" Type="Edm.String" />
             </EntityType>
          </Schema>
          <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default">
             <EntityContainer Name="Container">
                <EntitySet Name="Products" EntityType="NS.Models.Product" />
             </EntityContainer>
          </Schema>
       </edmx:DataServices>
    </edmx:Edmx>