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

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>