I'm using the OData framework (version 5.0.0) together with MVC Web Api (MVC version 4.0.305060), and cannot get the $expand query to work for my collections.
According to this wiki page, and more specifically case #2 under "Supported scenarios", I should be able to expand on Collections to include their data in a single query, but my $expand query seems to be completely ignored. No error, the response is exactly the same as if I left out the $expand altogether.
This is (part of) my data structure:
DebtCalculation
- string Name
- string Description
- Collection<Expense> Expenses
- Collection<Participant> Participants
Participant
- bool HasPaid
- User User
- DebtCalculation DebtCalculation
Expense
- decimal Amount
- User Payer
- Collection<Debtor> Debtors
- DebtCalculation DebtCalculation
To summarize; querying with $expand for a single entity works as expected, both through myservice/api/Expenses?$expand=Payer
and myservice/api/Expenses(10)?$expand=Payer
, but querying for myservice/api/DebtCalculations?$expand=Participants
does not.
This is (part of) my controller and ODataConventionModelBuilder setup:
[Authorize]
public class DebtCalculationsController : EntitySetController<DebtCalculation, long>
{
private DebtCalculationManager _debtCalculationManager { get; set; }
public DebtCalculationsController(DebtCalculationManager debtCalculationManager)
{
_debtCalculationManager = debtCalculationManager;
}
public override IQueryable<DebtCalculation> Get()
{
return _debtCalculationManager.AllDebtCalculationsForApi();
}
protected override DebtCalculation GetEntityByKey(long key)
{
return _debtCalculationManager.GetDebtCalculation(key);
}
public IQueryable<Participant> GetParticipants(long key)
{
return _debtCalculationManager.GetDebtCalculation(key).Participants.AsQueryable();
}
public IQueryable<Expense> GetExpenses(long key)
{
return _debtCalculationManager.GetDebtCalculation(key).Expenses.AsQueryable();
}
}
[Authorize]
public class ExpensesController : EntitySetController<Expense, long>
{
private DebtCalculationManager _debtCalculationManager { get; set; }
public ExpensesController(DebtCalculationManager debtCalculationManager)
{
_debtCalculationManager = debtCalculationManager;
}
public override IQueryable<Expense> Get()
{
return _debtCalculationManager.AllDebtCalculationsForApi()
.SelectMany(dc => dc.Expenses);
}
protected override Expense GetEntityByKey(long key)
{
return _debtCalculationManager.ExpenseForApi(key);
}
public User GetPayer(long key)
{
return _debtCalculationManager.ExpenseForApi(key).Payer;
}
public IQueryable<Debtor> GetDebtors(long key)
{
return _debtCalculationManager.ExpenseForApi(key).Debtors.AsQueryable();
}
public DebtCalculation GetDebtCalculation(long key)
{
return _debtCalculationManager.ExpenseForApi(key).DebtCalculation;
}
}
[Authorize]
public class ParticipantsController : EntitySetController<Participant, long>
{
private DebtCalculationManager _debtCalculationManager { get; set; }
public ParticipantsController(DebtCalculationManager debtCalculationManager)
{
_debtCalculationManager = debtCalculationManager;
}
public override IQueryable<Participant> Get()
{
return _debtCalculationManager.AllDebtCalculationsForApi()
.SelectMany(dc => dc.Participants);
}
protected override Participant GetEntityByKey(long key)
{
return _debtCalculationManager.ParticipantForApi(key);
}
public DebtCalculation GetDebtCalculation(long key)
{
return _debtCalculationManager.ParticipantForApi(key).DebtCalculation;
}
}
public static class WebApiConfig
{
public static HttpConfiguration Config { get; set; }
public static void Register(HttpConfiguration config)
{
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<DebtCalculation>("DebtCalculations")
.EntityType.HasKey(dc => dc.Id);
modelBuilder.EntitySet<Participant>("Participants")
.EntityType.HasKey(p => p.Id)
.Ignore(p => p.UniqueKey);
modelBuilder.EntitySet<Expense>("Expenses")
.EntityType.HasKey(e => e.Id);
...
var model = modelBuilder.GetEdmModel();
var routingConventions = ODataRoutingConventions.CreateDefault();
routingConventions.Insert(0, new CreateNavigationPropertyRoutingConvention());
config.Routes.MapODataRoute("ODataRoute", "api", model, new DefaultODataPathHandler(), routingConventions);
config.EnableQuerySupport();
}
// routing convention to handle POST requests to navigation properties.
public class CreateNavigationPropertyRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (odataPath.PathTemplate == "~/entityset/key/navigation" && controllerContext.Request.Method == HttpMethod.Post)
{
IEdmNavigationProperty navigationProperty = (odataPath.Segments[2] as NavigationPathSegment).NavigationProperty;
controllerContext.RouteData.Values["key"] = (odataPath.Segments[1] as KeyValuePathSegment).Value; // set the key for model binding.
return "PostTo" + navigationProperty.Name;
}
return null;
}
}
The metadata for DebtCalculation
looks like this:
<EntityType Name="DebtCalculation">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Int64" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="Description" Type="Edm.String"/>
<NavigationProperty Name="Participants" Relationship="Sujut.Core.Sujut_Core_DebtCalculation_Participants_Sujut_Core_Participant_ParticipantsPartner" ToRole="Participants" FromRole="ParticipantsPartner"/>
<NavigationProperty Name="Expenses" Relationship="Sujut.Core.Sujut_Core_DebtCalculation_Expenses_Sujut_Core_Expense_ExpensesPartner" ToRole="Expenses" FromRole="ExpensesPartner"/>
</EntityType>
Recognize any problems?
Edit:
The problem seems to be combining ODataController querying with Linq-to-NHibernate. Here is the stack trace. An upgrade to the NH 4.0.0.1000 alpha release did not solve the problem. This bug report seems to be about the same issue.
System.ArgumentException
at System.Linq.Expressions.Expression.Condition(Expression test, Expression ifTrue, Expression ifFalse)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitConditionalExpression(ConditionalExpression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.Visitors.NhExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.SelectClauseRewriter.VisitExpression(Expression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitBinaryExpression(BinaryExpression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.Visitors.NhExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.SelectClauseRewriter.VisitExpression(Expression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitConditionalExpression(ConditionalExpression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.Visitors.NhExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.SelectClauseRewriter.VisitExpression(Expression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberAssignment(MemberAssignment memberAssigment)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberBinding(MemberBinding memberBinding)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitList[T](ReadOnlyCollection`1 list, Func`2 visitMethod)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberBindingList(ReadOnlyCollection`1 expressions)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberInitExpression(MemberInitExpression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.Visitors.NhExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.SelectClauseRewriter.VisitExpression(Expression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberAssignment(MemberAssignment memberAssigment)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberBinding(MemberBinding memberBinding)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitList[T](ReadOnlyCollection`1 list, Func`2 visitMethod)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberBindingList(ReadOnlyCollection`1 expressions)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitMemberInitExpression(MemberInitExpression expression)
at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.Visitors.NhExpressionTreeVisitor.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.SelectClauseRewriter.VisitExpression(Expression expression)
at NHibernate.Linq.NestedSelects.NestedSelectRewriter.ReWrite(QueryModel queryModel, ISessionFactory sessionFactory)
at NHibernate.Linq.Visitors.QueryModelVisitor.GenerateHqlQuery(QueryModel queryModel, VisitorParameters parameters, Boolean root)
at NHibernate.Linq.NhLinqExpression.Translate(ISessionFactoryImplementor sessionFactory)
at NHibernate.Hql.Ast.ANTLR.ASTQueryTranslatorFactory.CreateQueryTranslators(String queryIdentifier, IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 filters, ISessionFactoryImplementor factory)
at NHibernate.Engine.Query.HQLExpressionQueryPlan.CreateTranslators(String expressionStr, IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 enabledFilters, ISessionFactoryImplementor factory)
at NHibernate.Engine.Query.HQLExpressionQueryPlan..ctor(String expressionStr, IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 enabledFilters, ISessionFactoryImplementor factory)
at NHibernate.Engine.Query.HQLExpressionQueryPlan..ctor(String expressionStr, IQueryExpression queryExpression, Boolean shallow, IDictionary`2 enabledFilters, ISessionFactoryImplementor factory)
at NHibernate.Engine.Query.QueryPlanCache.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow, IDictionary`2 enabledFilters)
at NHibernate.Impl.AbstractSessionImpl.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow)
at NHibernate.Impl.AbstractSessionImpl.CreateQuery(IQueryExpression queryExpression)
at NHibernate.Linq.DefaultQueryProvider.PrepareQuery(Expression expression, IQuery& query, NhLinqExpression& nhQuery)
at NHibernate.Linq.DefaultQueryProvider.Execute(Expression expression)
at NHibernate.Linq.DefaultQueryProvider.Execute[TResult](Expression expression)
at Remotion.Linq.QueryableBase`1.System.Collections.IEnumerable.GetEnumerator()
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__14.MoveNext()
Problem is with Linq-to-NHibernate, bug reported here.
Worked around the problem by calling
.ToList().AsQueryable()
on the returnedIQueryable<DebtCalculation>
set. This is obviously not very efficient, but will allow me to continue development and testing of apps relying on the API until bug is fixed.