0

I am working on an ASP.NET Core 2.2 API that is implementing OData via Microsoft.AspNetCore.Odata v7.1.0 NuGet. I had everything working fine so I decided to add API Versioning via the Microsoft.AspNetCore.OData.Versioning v3.1.0.

Now, my GET and GET{id} methods in my controller work correctly with versioning. For example, I can get to the GET list endpoint method by using the URL

~/api/v1/addresscompliancecodes

or

~/api/addresscompliancecodes?api-version=1.0

However when I try to create a new record, the request routes to the correct method in the controller but now the request body content is not being passed to the POST controller method

I have been following the examples in the Microsoft.ApsNetCore.OData.Versioning GitHub

There is the HttpPost method in my controller;

    [HttpPost]
    [ODataRoute()]
    public async Task<IActionResult> CreateRecord([FromBody] AddressComplianceCode record, ODataQueryOptions<AddressComplianceCode> options)
    {

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        _context.Add(record);
        await _context.SaveChangesAsync();

        return Created(record);
    }

When I debug, the request routes to the controller method properly but the "record" variable is now null, whereas, before adding the code changes for API Versioning, it was correctly populated.

I suspect it is how I am using the model builder since that code changed to support API versioning.

Before trying to implement API Versioning, I was using a model builder class as shown below;

public class AddressComplianceCodeModelBuilder
{

    public IEdmModel GetEdmModel(IServiceProvider serviceProvider)
    {
        var builder = new ODataConventionModelBuilder(serviceProvider);

        builder.EntitySet<AddressComplianceCode>(nameof(AddressComplianceCode))
            .EntityType
            .Filter()
            .Count()
            .Expand()
            .OrderBy()
            .Page() // Allow for the $top and $skip Commands
            .Select(); 

        return builder.GetEdmModel();
    }

}

And a Startup.cs --> Configure method like what is shown in the snippet below;

        // Support for OData $batch
        app.UseODataBatching();

        app.UseMvc(routeBuilder =>
        {
            // Add support for OData to MVC pipeline
            routeBuilder
                .MapODataServiceRoute("ODataRoutes", "api/v1",
                    modelBuilder.GetEdmModel(app.ApplicationServices),
                    new DefaultODataBatchHandler());



        });

And it worked with [FromBody] in the HttpPost method of the controller.

However, in following the examples in the API Versioning OData GitHub, I am now using a Configuration class like what is shown below, rather than the model builder from before;

public class AddressComplianceCodeModelConfiguration : IModelConfiguration
{

    private static readonly ApiVersion V1 = new ApiVersion(1, 0);

    private EntityTypeConfiguration<AddressComplianceCode> ConfigureCurrent(ODataModelBuilder builder)
    {
        var addressComplianceCode = builder.EntitySet<AddressComplianceCode>("AddressComplianceCodes").EntityType;

        addressComplianceCode
            .HasKey(p => p.Code)
            .Filter()
            .Count()
            .Expand()
            .OrderBy()
            .Page() // Allow for the $top and $skip Commands
            .Select();


        return addressComplianceCode;
    }
    public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
    {
        if (apiVersion == V1)
        {
            ConfigureCurrent(builder);
        }
    }
}

And my Startup.cs --> Configure method is changed as shown below;

    public void Configure(IApplicationBuilder app,
        IHostingEnvironment env, 
        VersionedODataModelBuilder modelBuilder)
    {

        // Support for OData $batch
        app.UseODataBatching();

        app.UseMvc(routeBuilder =>
        {
            // Add support for OData to MVC pipeline
            var models = modelBuilder.GetEdmModels();
            routeBuilder.MapVersionedODataRoutes("odata", "api", models);
            routeBuilder.MapVersionedODataRoutes("odata-bypath", "api/v{version:apiVersion}", models);
        });


    }

If it is relevant, I have the following code in my Startup.cs -> ConfigureServices;

        // Add Microsoft's API versioning
        services.AddApiVersioning(options =>
        {
            // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
            options.ReportApiVersions = true;
        });

        // Add OData 4.0 Integration
        services.AddOData().EnableApiVersioning();

        services.AddMvc(options =>
            {
                options.EnableEndpointRouting = false; // TODO: Remove when OData does not causes exceptions anymore
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
            .AddJsonOptions(opt =>
            {
                opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            });

I feel the issue is with the model is somehow not matching up correctly but I cannot see exactly why it isn't

UPDATE 3/18/19 - Additional Information

Here is my entity class;

[Table("AddressComplianceCodes")]
public class AddressComplianceCode : EntityBase
{
    [Key]
    [Column(TypeName = "char(2)")]
    [MaxLength(2)]
    public string Code { get; set; }

    [Required]
    [Column(TypeName = "varchar(150)")]
    [MaxLength(150)]
    public string Description { get; set; }
}

and the EntityBase class;

public class EntityBase : IEntityDate
{
    public bool MarkedForRetirement { get; set; }

    public DateTimeOffset? RetirementDate { get; set; }

    public DateTimeOffset? LastModifiedDate { get; set; }

    public string LastModifiedBy { get; set; }

    public DateTimeOffset? CreatedDate { get; set; }

    public string CreatedBy { get; set; }

    public bool Delete { get; set; }

    public bool Active { get; set; }
}

And here is the request body from Postman;

{   
    "@odata.context": "https://localhost:44331/api/v1/$metadata#AddressComplianceCodes",
    "Code": "Z1",
    "Description": "Test Label - This is a test for Z1",
    "Active": true
}

Any ideas?

whiskytangofoxtrot
  • 927
  • 1
  • 13
  • 41

2 Answers2

2

As it turned out, the problem was because I was not using camel case as my property names in the Postman request body. This was not an issue with Microsoft.AspNetCore.Odata alone but once I added the Microsoft.AspNetCore.Odata.Versioning NuGet package, it failed with the upper case starting character of the property names. It seems that Microsoft.AspNetCore.Odata.Versioning uses it's own MediaTypeFormatter that enables lower camel case. I discovered this in the following GitHub post; https://github.com/Microsoft/aspnet-api-versioning/issues/310

whiskytangofoxtrot
  • 927
  • 1
  • 13
  • 41
0

There is no custom MediaTypeFormatter, but the behavior did change in 3.0 as using camel casing is seemingly the default for most JSON-based APIs. This is easy to revert back however.

modelBuilder.ModelBuilderFactory = () => new ODataConventionModelBuilder();
// as opposed to the new default:
// modelBuilder.ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase();

The is also the place were you'd perform or change any other setup related to model builders. The factory method is called to create a new model builder per API version.

It's worth pointing out that you do not need to map routes twice. For demonstraton purposes, the by query string and by URL path are configured. You should choose one or the other and remove the one that isn't used.

I hope that helps.

Chris Martinez
  • 3,185
  • 12
  • 28