Jeroen E
Jeroen E

Reputation: 57

automatically expand related entity with OData controller

I have these classes:

    public class Items
    {
    [Key]
    public Guid Id { get; set; }
    public string ItemCode { get; set; }
    public decimal SalesPriceExcl { get; set; }
    public decimal SalesPriceIncl { get; set; }

    public virtual ICollection<ItemPrice> SalesPrices { get; set; }

    public Items()
    {
        SalesPrices = new HashSet<App4Sales_ItemPrice>();
    }
}

 public class ItemPrice
{
    [Key, Column(Order = 0), ForeignKey("Items")]
    public Guid Id { get; set; }
    public virtual Items Items { get; set; }

    [Key, Column(Order=1)]
    public Guid PriceList { get; set; }        
    public decimal PriceExcl { get; set; }
    public decimal PriceIncl { get; set; }
    public decimal VatPercentage { get; set; }        

}

I want to query the Items and automatically get the ItemPrice collection.

I've created an OData V3 controller:

    // GET: odata/Items
    //[Queryable]
    public IQueryable<Items> GetItems(ODataQueryOptions opts)
    {
        SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);
        Request.SetSelectExpandClause(expandOpts.SelectExpandClause);
        return expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings()) as IQueryable<Items>;


    }

But I get the error: "Cannot serialize null feed"

Yes, some Items have no ItemPrice list.

Can I get past this error, or can I do something different?

Kind regards

Jeroen

I found the underlying error is: Unable to cast object of type 'System.Data.Entity.Infrastructure.DbQuery1[System.Web.Http.OData.Query.Expressions.SelectExpandBinder+SelectAllAndExpand1[.Models.Items]]' to type '.Models.Items'.

Upvotes: 1

Views: 1187

Answers (3)

Davious
Davious

Reputation: 1873

expanding on Jeroen's post. Anytime a select or expand is involved, OData wraps the results in a SelectAll or SelectSome object; so, we need to unwrap the values rather than do an direct cast.

public static class ODataQueryOptionsExtensions
{
    public static IEnumerable<T> ApplyODataOptions<T>(this IQueryable<T> query, ODataQueryOptions options) where T : class, new()
    {
        if (options == null)
        {
            return query;
        }

        var queryable = options.ApplyTo(query);
        if (queryable is IQueryable<T> queriableEntity)
        {
            return queriableEntity.AsEnumerable();
        }
        return UnwrapAll<T>(queryable).ToList();
    }

    public static IEnumerable<T> UnwrapAll<T>(this IQueryable queryable) where T : class, new()
    {
        foreach (var item in queryable)
        {
            yield return Unwrap<T>(item);
        }
    }

    public static T Unwrap<T>(object item) where T : class, new()
    {
        var instanceProp = item.GetType().GetProperty("Instance");
        var value = (T)instanceProp.GetValue(item);
        if (value != null)
        {
            return value;
        }

        value = new T();
        var containerProp = item.GetType().GetProperty("Container");
        var container = containerProp.GetValue(item);
        if (container == null)
        {
            return (T)null;
        }
        var containerType = container.GetType();
        var containerItem = container;
        var allNull = true;
        for (var i = 0; containerItem != null; i++)
        {
            var containerItemType = containerItem.GetType();
            var containerItemValue = containerItemType.GetProperty("Value").GetValue(containerItem);
            if (containerItemValue == null)
            {
                containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
                continue;
            }
            var containerItemName = containerItemType.GetProperty("Name").GetValue(containerItem) as string;
            var expandedProp = typeof(T).GetProperty(containerItemName);
            if (expandedProp.SetMethod == null)
            {
                containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
                continue;
            }
            if (containerItemValue.GetType() != typeof(string) && containerItemValue is IEnumerable containerValues)
            {
                var listType = typeof(List<>).MakeGenericType(expandedProp.PropertyType.GenericTypeArguments[0]);
                var expandedList = (IList)Activator.CreateInstance(listType);
                foreach (var expandedItem in containerValues)
                {
                    var expandedInstanceProp = expandedItem.GetType().GetProperty("Instance");
                    var expandedValue = expandedInstanceProp.GetValue(expandedItem);
                    expandedList.Add(expandedValue);
                }
                expandedProp.SetValue(value, expandedList);
                allNull = false;
            }
            else
            {
                var expandedInstanceProp = containerItemValue.GetType().GetProperty("Instance");
                if (expandedInstanceProp == null)
                {
                    expandedProp.SetValue(value, containerItemValue);
                    allNull = false;
                }
                else
                {
                    var expandedValue = expandedInstanceProp.GetValue(containerItemValue);
                    if (expandedValue != null)
                    {
                        expandedProp.SetValue(value, expandedValue);
                        allNull = false;
                    }
                    else
                    {
                        var t = containerItemValue.GetType().GenericTypeArguments[0];
                        var wrapInfo = typeof(ODataQueryOptionsExtensions).GetMethod(nameof(Unwrap));
                        var wrapT = wrapInfo.MakeGenericMethod(t);
                        expandedValue = wrapT.Invoke(null, new[] { containerItemValue });
                        if (expandedValue != null)
                        {
                            expandedProp.SetValue(value, expandedValue);
                            allNull = false;
                        }
                    }
                }
            }
            containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
        }
        if (allNull)
        {
            return (T)null;
        }
        return value;
    }
}

Upvotes: 0

Jeroen E
Jeroen E

Reputation: 57

I've solved it after I came across this post: http://www.jauernig-it.de/intercepting-and-post-processing-odata-queries-on-the-server/

This is my controller now:

SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);

        Request.SetSelectExpandClause(expandOpts.SelectExpandClause);

        var result = expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings());
        var resultList = new List<Items>();

        foreach (var item in result)
        {
            if (item is Items)
            {
                resultList.Add((Items)item);
            }
            else if (item.GetType().Name == "SelectAllAndExpand`1")
            {
                var entityProperty = item.GetType().GetProperty("Instance");
                resultList.Add((Items)entityProperty.GetValue(item));
            }
        }


        return resultList.AsQueryable();

Jeroen

Upvotes: 1

John Little
John Little

Reputation: 878

GetItems([FromODataUri] ODataQueryOptions queryOptions)

Upvotes: 0

Related Questions