Webapi: Can't call OData function with decimal parameter after upgrading to Microsoft.OData 7 & Microsoft.AspNet.OData 6

Created on 19 Sep 2016  路  8Comments  路  Source: OData/WebApi

After upgrading to Microsoft.OData 7 & Microsoft.AspNet.OData 6, I can't call my web api odata functions with decimal parameters due the following exception:

Unable to cast object of type 'Microsoft.OData.Edm.EdmPrimitiveTypeReference' to type 'Microsoft.OData.Edm.IEdmDecimalTypeReference' in Microsoft.OData.UriParser.MetadataBindingUtils.ConvertToTypeIfNeeded(SingleValueNode source, IEdmTypeReference targetTypeReference)

Take a look at following test code:
Note: Ready to build an test project is available at: https://github.com/ymoradi/ODataFunctionWithDecimalParameterProblem

public class AspNetCoreStartup
    {
        public void ConfigureServices(IServiceCollection services)
        {

        }

        public void Configure(IApplicationBuilder aspNetCoreApp)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                HttpConfiguration webApiConfig = new HttpConfiguration();
                webApiConfig.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
                HttpServer server = new HttpServer(webApiConfig);
                ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder(webApiConfig);
                modelBuilder.Namespace = modelBuilder.ContainerName = "Test";
                EntitySetConfiguration<Product> products = modelBuilder.EntitySet<Product>("Products");
                FunctionConfiguration testDecimal = products.EntityType.Collection.Function("TestDecimal");
                testDecimal.Returns<decimal>();
                testDecimal.Parameter<decimal>("val").OptionalParameter = false;
                FunctionConfiguration testLong = products.EntityType.Collection.Function("TestLong");
                testLong.Returns<long>();
                testLong.Parameter<long>("val").OptionalParameter = false;
                ODataBatchHandler odataBatchHandler = new DefaultODataBatchHandler(server);
                odataBatchHandler.ODataRouteName = "test-odata";
                IList<IODataRoutingConvention> conventions = ODataRoutingConventions.CreateDefault();
                DefaultODataPathHandler pathHandler = new DefaultODataPathHandler { };
                IEdmModel edmModel = modelBuilder.GetEdmModel();
                webApiConfig.MapODataServiceRoute("test-odata", "test", edmModel, pathHandler, conventions, odataBatchHandler);
                webApiConfig.EnsureInitialized();

                owinApp.Map("/odata", innerOwinApp =>
                {
                    innerOwinApp.UseWebApi(server);
                });

            });
        }
    }

    public class Product
    {
        [Key]
        public virtual int Id { get; set; }
    }

    public class ProductsController : ODataController
    {
        [HttpGet]
        public decimal TestDecimal([FromODataUri]decimal val)
        {
            return val;
        }

        [HttpGet]
        public long TestLong([FromODataUri]long val)
        {
            return val;
        }
    }

    public static class OwinExtensions
    {
        public static IApplicationBuilder UseOwinApp(this IApplicationBuilder aspNetCoreApp, Action<IAppBuilder> configuration)
        {
            return aspNetCoreApp.UseOwin(setup => setup(next =>
            {
                AppBuilder owinAppBuilder = new AppBuilder();
                IApplicationLifetime aspNetCoreLifetime = (IApplicationLifetime)aspNetCoreApp.ApplicationServices.GetService(typeof(IApplicationLifetime));
                AppProperties owinAppProperties = new AppProperties(owinAppBuilder.Properties);
                owinAppProperties.OnAppDisposing = aspNetCoreLifetime?.ApplicationStopping ?? CancellationToken.None;
                owinAppProperties.DefaultApp = next;
                configuration(owinAppBuilder);
                return owinAppBuilder.Build<Func<IDictionary<string, object>, Task>>();
            }));
        }
    }

    [TestClass]
    public class TestClass
    {
        [TestMethod]
        public async Task DecimalParameterWhichHasAnException()
        {
            TestServer aspNetCoreTestServer = new TestServer(new WebHostBuilder().UseStartup<AspNetCoreStartup>());
            HttpClient testClient = aspNetCoreTestServer.CreateClient();
            HttpResponseMessage response = await testClient.GetAsync("/odata/test/Products/Test.TestDecimal(val=1)");
            response.EnsureSuccessStatusCode();
            dynamic result = await response
                .Content.ReadAsAsync<dynamic>();
            Assert.AreEqual(1, Convert.ToDecimal(result.value));
            // same problem for val=1m, with or without IEEE754Compatibility header
        }

        [TestMethod]
        public async Task LongParameterWithoutAnyProblem()
        {
            TestServer aspNetCoreTestServer = new TestServer(new WebHostBuilder().UseStartup<AspNetCoreStartup>());
            HttpClient testClient = aspNetCoreTestServer.CreateClient();
            HttpResponseMessage response = await testClient.GetAsync("/odata/test/Products/Test.TestLong(val=1)");
            response.EnsureSuccessStatusCode();
            dynamic result = await response
                .Content.ReadAsAsync<dynamic>();
            Assert.AreEqual(1, Convert.ToInt32(result.value));
        }
    }
P2 bug regression

All 8 comments

Passing decimal values to odata actions is possible, is this a bug somehow?

@ymoradi Just ran into this same problem. I pulled down the odatasample function project, modified the contract to include a decimal parameter and ran it just to make sure I'm not crazy and it runs fine with the prior version. It breaks after upgrading.

"double" type works just fine. As a workaround, I'm changing my function to expect "double" and cast to decimal internally. Hopefully by the time we need a decimal, this will be fixed.

@ymoradi @MAOliver It's a bug in ODataLib and current I suggest you use this work around:

after get the edm model:

var model = builder.GetEdmModel();

Add function use ODataLib's API:
http://odata.github.io/odata.net/v7/#02-06-define-operations
use EdmCoreModel.Instance.GetDecimal for the decimal paratmer.

Things should working.

Thanks @VikingsFan
I'm using ODataConventionalModelBuilder, and AddParameter method of OperationConfiguration needs IEdmTypeConfiguration, and EdmCoreModel.Instance.GetDecimal return IEdmDecimalTypeReference. There is no way to cast IEdmDecimalTypeReference to IEdmTypeConfiguration. The test that I've provided to you before, fails because of invalid cast operation from IEdmTypeConfiguration to IEdmDecimalTypeReference at runtime too (-:

yes but we should use EdmModel to add function, like: http://odata.github.io/odata.net/v7/#02-06-define-operations

because the bug we can't use ConventionalModelBuilder, so the logic should like:

ODataConventionModelBuilder builder = new ..
builder.entityset...
// build the model except the function.
..
IEdmModel model = builder.GetEdmModel();
EdmModel edmModel = model as EdmModel;
var edmFunction = new ...
// new the function use the code in the link above.
edmModel.AddElement(edmFunction);

then the edmModel should work.

@ysmoradi .
My work around.

  1. Add function without decimal parameter.
  2. Build Edm model
  3. Find declared function in the model and cast to EdmFunction.
  4. Call AddParameter

@genusP Thanks for the work around.

This solved the problem for me.

PR #1594 fix this issue.
Use version 7.1

Was this page helpful?
0 / 5 - 0 ratings