Search code examples
c#asp.net-coreodataapi-versioningmicrosoft-odata

Why is my HTTP Post no longer passing the body content after adding Microsoft.AspNetCore.OData.Versioning


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?


Solution

  • 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