Расширение коллекций с помощью EntitySetController в MVC Web Api
Я использую платформу OData (версия 5.0.0) вместе с MVC Web Api (версия MVC 4.0.305060) и не могу заставить запрос $expand работать для моих коллекций.
Согласно этой вики-странице и, в частности, случаю № 2 в разделе "Поддерживаемые сценарии", я должен иметь возможность развернуть Коллекции, чтобы включить их данные в один запрос, но мой запрос $expand, похоже, полностью игнорируется. Нет ошибок, ответ точно такой же, как если бы я вообще пропустил расширение $.
Это (часть) моей структуры данных:
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
Подвести итоги; запросы с $expand для одной сущности работают как ожидалось, как через myservice/api/Expenses?$expand=Payer
а также myservice/api/Expenses(10)?$expand=Payer
, но запрашивая myservice/api/DebtCalculations?$expand=Participants
не.
Это (часть) мой контроллер и настройка ODataConventionModelBuilder:
[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;
}
}
Метаданные для DebtCalculation
выглядит так:
<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>
Распознать какие-либо проблемы?
Редактировать:
Кажется, проблема заключается в объединении запросов ODataController с Linq-to-NHibernate. Вот трассировка стека. Обновление до альфа-версии NH 4.0.0.1000 не решило проблему. Этот отчет об ошибке, кажется, о той же самой проблеме.
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()
2 ответа
Проблема с Linq-to-NHibernate, ошибка сообщается здесь.
Обошли проблему, позвонив .ToList().AsQueryable()
на возвращенный IQueryable<DebtCalculation>
задавать. Это, очевидно, не очень эффективно, но позволит мне продолжить разработку и тестирование приложений, использующих API, до устранения ошибки.
Я столкнулся с той же проблемой и попробовал обходной путь, но для больших наборов данных он вызывал много операторов выбора из-за отложенной загрузки.
Поэтому я выбрал другой подход и вручную включил соответствующие таблицы.
// Using Chinook Database
Album albumAlias = null;
Track trackAlias = null;
Artist artistAlias = null;
return session.QueryOver<Album>(() => albumAlias)
.Left.JoinAlias(() => albumAlias.Tracks, () => trackAlias)
.Left.JoinAlias(() => albumAlias.Artist, () => artistAlias)
.TransformUsing(Transformers.DistinctRootEntity)
.List().AsQueryable();
Альбом имеет много треков и одного исполнителя, связанных с ним. Таким образом, он будет генерировать только один оператор выбора и все еще сможет расширять любое из свойств.
Для вашего примера это будет выглядеть примерно так
DebtCalculation debtCalculationAlias = null;
Expense expenseAlias = null;
Participant participantAlias = null;
session.QueryOver<DebtCalculation>(() => debtCalculationAlias)
.Left.JoinAlias(() => debtCalculationAlias.Expenses, () => expenseAlias)
.Left.JoinAlias(() => debtCalculationAlias.Participants, () => participantAlias)
.TransformUsing(Transformers.DistinctRootEntity)
.List().AsQueryable();
Я только вхожу в OData, поэтому, если вы считаете, что это неправильный путь, не стесняйтесь поправлять меня.