Search code examples
asp.net-web-api2odata

OData v4 error using filter with groupby


I'm getting an error when using $filter with $apply=groupby. It appears to happen only when the filter field is not in the groupby expression. Here is the error message: Instance property 'DRG_Definition' is not defined for type 'DynamicTypeWrapper'

This works fine: http://localhost:9810/odata/PAYMENTS?$apply=groupby((Provider_Id,DRG_Definition),aggregate(Total_Payments with sum as Total_Payments))&$filter=(DRG_Definition eq '069 - TRANSIENT ISCHEMIA')

This throws the error (only difference is no DRG_Definition field in the groupby): http://localhost:9810/odata/PAYMENTS?$apply=groupby((Provider_Id),aggregate(Total_Payments with sum as Total_Payments))&$filter=(DRG_Definition eq '069 - TRANSIENT ISCHEMIA')

Updated with my packages and code samples below:

<packages>
  <package id="EntityFramework" version="6.1.3" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.Agent.Intercept" version="1.2.1" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.DependencyCollector" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.JavaScript" version="0.15.0-build58334" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.PerfCounterCollector" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.Web" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.WindowsServer" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.AspNet.OData" version="5.9.1" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net452" />
  <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.1" targetFramework="net452" />
  <package id="Microsoft.Net.Compilers" version="1.3.2" targetFramework="net452" developmentDependency="true" />
  <package id="Microsoft.OData.Core" version="6.15.0" targetFramework="net452" />
  <package id="Microsoft.OData.Edm" version="6.15.0" targetFramework="net452" />
  <package id="Microsoft.Spatial" version="6.15.0" targetFramework="net452" />
  <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net452" />
  <package id="System.Spatial" version="5.7.0" targetFramework="net452" />
</packages>

Here's the WebApiConfig.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using HealthcareWebApp;

namespace HealthcareWebApp
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //Custom code
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<PAYMENTS>("PAYMENTS"); //.EntityType.HasKey(p => p.PAYMENT_KEY);
            builder.EntitySet<DATE_DIM>("DATE_DIM"); //.EntityType.HasKey(p => p.Year);
            builder.EntitySet<PROVIDERS>("PROVIDERS"); //.EntityType.HasKey(p => p.Provider_Id);
            config.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());

        }
    }
}

PAYMENTSController.cs:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.ModelBinding;
using System.Web.OData;
using System.Web.OData.Query;
using System.Web.OData.Routing;
using HealthcareWebApp;

namespace HealthcareWebApp.Controllers
{
    /*
    The WebApiConfig class may require additional changes to add a route for this controller. Merge these statements into the Register method of the WebApiConfig class as applicable. Note that OData URLs are case sensitive.

    using System.Web.OData.Builder;
    using System.Web.OData.Extensions;
    using HealthcareWebApp;
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<PAYMENTS>("PAYMENTS");
    builder.EntitySet<DATE_DIM>("DATE_DIM"); 
    builder.EntitySet<PROVIDERS>("PROVIDERS"); 
    config.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
    */
    public class PAYMENTSController : ODataController
    {
        private FlexIT_HealthcareEntities db = new FlexIT_HealthcareEntities();

        // GET: odata/PAYMENTS
        [EnableQuery]
        public IQueryable<PAYMENTS> GetPAYMENTS()
        {
            return db.PAYMENTS;
        }

        // GET: odata/PAYMENTS(5)
        [EnableQuery]
        public SingleResult<PAYMENTS> GetPAYMENTS([FromODataUri] int key)
        {
            return SingleResult.Create(db.PAYMENTS.Where(pAYMENTS => pAYMENTS.PAYMENT_KEY == key));
        }

        // PUT: odata/PAYMENTS(5)
        public IHttpActionResult Put([FromODataUri] int key, Delta<PAYMENTS> patch)
        {
            Validate(patch.GetEntity());

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

            PAYMENTS pAYMENTS = db.PAYMENTS.Find(key);
            if (pAYMENTS == null)
            {
                return NotFound();
            }

            patch.Put(pAYMENTS);

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!PAYMENTSExists(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Updated(pAYMENTS);
        }

        // POST: odata/PAYMENTS
        public IHttpActionResult Post(PAYMENTS pAYMENTS)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.PAYMENTS.Add(pAYMENTS);
            db.SaveChanges();

            return Created(pAYMENTS);
        }

        // PATCH: odata/PAYMENTS(5)
        [AcceptVerbs("PATCH", "MERGE")]
        public IHttpActionResult Patch([FromODataUri] int key, Delta<PAYMENTS> patch)
        {
            Validate(patch.GetEntity());

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

            PAYMENTS pAYMENTS = db.PAYMENTS.Find(key);
            if (pAYMENTS == null)
            {
                return NotFound();
            }

            patch.Patch(pAYMENTS);

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!PAYMENTSExists(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Updated(pAYMENTS);
        }

        // DELETE: odata/PAYMENTS(5)
        public IHttpActionResult Delete([FromODataUri] int key)
        {
            PAYMENTS pAYMENTS = db.PAYMENTS.Find(key);
            if (pAYMENTS == null)
            {
                return NotFound();
            }

            db.PAYMENTS.Remove(pAYMENTS);
            db.SaveChanges();

            return StatusCode(HttpStatusCode.NoContent);
        }

        // GET: odata/PAYMENTS(5)/DATE_DIM
        [EnableQuery]
        public SingleResult<DATE_DIM> GetDATE_DIM([FromODataUri] int key)
        {
            return SingleResult.Create(db.PAYMENTS.Where(m => m.PAYMENT_KEY == key).Select(m => m.DATE_DIM));
        }

        // GET: odata/PAYMENTS(5)/PROVIDERS
        [EnableQuery]
        public SingleResult<PROVIDERS> GetPROVIDERS([FromODataUri] int key)
        {
            return SingleResult.Create(db.PAYMENTS.Where(m => m.PAYMENT_KEY == key).Select(m => m.PROVIDERS));
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool PAYMENTSExists(int key)
        {
            return db.PAYMENTS.Count(e => e.PAYMENT_KEY == key) > 0;
        }
    }
}

Lastly, the PAYMENTS.cs model:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace HealthcareWebApp
{
    using System;
    using System.Collections.Generic;

    public partial class PAYMENTS
    {
        [System.ComponentModel.DataAnnotations.Key] //manually added by ataft
        public int PAYMENT_KEY { get; set; }
        public string DRG_Definition { get; set; }
        public string Provider_Id { get; set; }
        public string Hospital_Referral_Region_Description { get; set; }
        public Nullable<decimal> Total_Discharges_ { get; set; }
        public Nullable<decimal> Covered_Charges { get; set; }
        public Nullable<decimal> Total_Payments { get; set; }
        public Nullable<decimal> Medicare_Payments { get; set; }
        public int Year { get; set; }

        public virtual DATE_DIM DATE_DIM { get; set; }
        public virtual PROVIDERS PROVIDERS { get; set; }
    }
}

Solution

  • It's an issue about filter and groupby is filter can't apply to groupby or aggregated property and resolve in WebAPI/OData 5.9.1.

    https://www.nuget.org/packages/Microsoft.AspNet.OData/5.9.1

    And in your scenario, apply will always executed first and then filter get executed, so when $apply=groupby((Provider_Id),aggregate(Total_Payments with sum as Total_Payments)), the result won't contain DRG_Definition, so the filter failed, if you want to filter first, you should use filter in apply, like $apply=filter(Name eq 'Lowest')/groupby((Name))

    FYI the spec http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html