Skip to content

Commit 66b9143

Browse files
Add support for OData group by queries (#2135)
Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com>
1 parent f47b06d commit 66b9143

File tree

8 files changed

+482
-7
lines changed

8 files changed

+482
-7
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Collections.Generic;
13+
using System.Linq;
14+
using Microsoft.AspNet.OData;
15+
using Microsoft.AspNet.OData.Builder;
16+
using Microsoft.AspNet.OData.Extensions;
17+
using Microsoft.AspNet.OData.Query;
18+
using Microsoft.AspNet.OData.Query.Expressions;
19+
using Microsoft.AspNetCore.Http;
20+
using Microsoft.OData.Edm;
21+
using NHibernate.DomainModel.Northwind.Entities;
22+
using NUnit.Framework;
23+
using NHibernate.Linq;
24+
25+
namespace NHibernate.Test.Linq
26+
{
27+
using System.Threading.Tasks;
28+
[TestFixture]
29+
public class ODataTestsAsync : LinqTestCase
30+
{
31+
private IEdmModel _edmModel;
32+
33+
protected override void OnSetUp()
34+
{
35+
base.OnSetUp();
36+
37+
_edmModel = CreatEdmModel();
38+
}
39+
40+
[TestCase("$apply=groupby((Customer/CustomerId))", 89)]
41+
[TestCase("$apply=groupby((Customer/CustomerId))&$orderby=Customer/CustomerId", 89)]
42+
[TestCase("$apply=groupby((Customer/CustomerId, ShippingAddress/PostalCode), aggregate(OrderId with average as Average, Employee/EmployeeId with max as Max))", 89)]
43+
[TestCase("$apply=groupby((Customer/CustomerId), aggregate(OrderId with sum as Total))&$skip=2", 87)]
44+
public async Task OrderGroupByAsync(string queryString, int expectedRows)
45+
{
46+
var query = ApplyFilter(session.Query<Order>(), queryString);
47+
Assert.That(query, Is.AssignableTo<IQueryable<DynamicTypeWrapper>>());
48+
49+
var results = await (((IQueryable<DynamicTypeWrapper>) query).ToListAsync());
50+
Assert.That(results, Has.Count.EqualTo(expectedRows));
51+
}
52+
53+
private IQueryable ApplyFilter<T>(IQueryable<T> query, string queryString)
54+
{
55+
var context = new ODataQueryContext(CreatEdmModel(), typeof(T), null) { };
56+
var dataQuerySettings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False};
57+
var serviceProvider = new ODataServiceProvider(
58+
new Dictionary<System.Type, object>()
59+
{
60+
{typeof(DefaultQuerySettings), new DefaultQuerySettings()},
61+
{typeof(ODataOptions), new ODataOptions()},
62+
{typeof(IEdmModel), _edmModel},
63+
{typeof(ODataQuerySettings), dataQuerySettings},
64+
});
65+
66+
HttpContext httpContext = new DefaultHttpContext();
67+
httpContext.ODataFeature().RequestContainer = serviceProvider;
68+
httpContext.RequestServices = serviceProvider;
69+
var request = httpContext.Request;
70+
Uri requestUri = new Uri($"http://localhost/?{queryString}");
71+
request.Method = HttpMethods.Get;
72+
request.Scheme = requestUri.Scheme;
73+
request.Host = new HostString(requestUri.Host);
74+
request.QueryString = new QueryString(requestUri.Query);
75+
request.Path = new PathString(requestUri.AbsolutePath);
76+
var options = new ODataQueryOptions(context, request);
77+
78+
return options.ApplyTo(query, dataQuerySettings);
79+
}
80+
81+
private static IEdmModel CreatEdmModel()
82+
{
83+
var builder = new ODataConventionModelBuilder();
84+
85+
var adressModel = builder.ComplexType<Address>();
86+
adressModel.Property(o => o.City);
87+
adressModel.Property(o => o.Country);
88+
adressModel.Property(o => o.Fax);
89+
adressModel.Property(o => o.PhoneNumber);
90+
adressModel.Property(o => o.PostalCode);
91+
adressModel.Property(o => o.Region);
92+
adressModel.Property(o => o.Street);
93+
94+
var customerModel = builder.EntitySet<Customer>(nameof(Customer));
95+
customerModel.EntityType.HasKey(o => o.CustomerId);
96+
customerModel.EntityType.Property(o => o.CompanyName);
97+
customerModel.EntityType.Property(o => o.ContactTitle);
98+
customerModel.EntityType.ComplexProperty(o => o.Address);
99+
customerModel.EntityType.HasMany(o => o.Orders);
100+
101+
var orderModel = builder.EntitySet<Order>(nameof(Order));
102+
orderModel.EntityType.HasKey(o => o.OrderId);
103+
orderModel.EntityType.Property(o => o.Freight);
104+
orderModel.EntityType.Property(o => o.OrderDate);
105+
orderModel.EntityType.Property(o => o.RequiredDate);
106+
orderModel.EntityType.Property(o => o.ShippedTo);
107+
orderModel.EntityType.Property(o => o.ShippingDate);
108+
orderModel.EntityType.ComplexProperty(o => o.ShippingAddress);
109+
orderModel.EntityType.HasRequired(o => o.Customer);
110+
orderModel.EntityType.HasOptional(o => o.Employee);
111+
112+
var employeeModel = builder.EntitySet<Employee>(nameof(Employee));
113+
employeeModel.EntityType.HasKey(o => o.EmployeeId);
114+
employeeModel.EntityType.Property(o => o.BirthDate);
115+
employeeModel.EntityType.Property(o => o.Extension);
116+
employeeModel.EntityType.Property(o => o.FirstName);
117+
employeeModel.EntityType.Property(o => o.HireDate);
118+
employeeModel.EntityType.Property(o => o.LastName);
119+
employeeModel.EntityType.Property(o => o.Notes);
120+
employeeModel.EntityType.Property(o => o.Title);
121+
employeeModel.EntityType.HasMany(o => o.Orders);
122+
123+
return builder.GetEdmModel();
124+
}
125+
126+
private class ODataServiceProvider : IServiceProvider
127+
{
128+
private readonly Dictionary<System.Type, object> _singletonObjects = new Dictionary<System.Type, object>();
129+
130+
public ODataServiceProvider(Dictionary<System.Type, object> singletonObjects)
131+
{
132+
_singletonObjects = singletonObjects;
133+
}
134+
135+
public object GetService(System.Type serviceType)
136+
{
137+
if (_singletonObjects.TryGetValue(serviceType, out var service))
138+
{
139+
return service;
140+
}
141+
142+
var ctor = serviceType.GetConstructor(new System.Type[0]);
143+
if (ctor != null)
144+
{
145+
return ctor.Invoke(new object[0]);
146+
}
147+
148+
ctor = serviceType.GetConstructor(new[] { typeof(DefaultQuerySettings) });
149+
if (ctor != null)
150+
{
151+
return ctor.Invoke(new object[] { GetService(typeof(DefaultQuerySettings)) });
152+
}
153+
154+
ctor = serviceType.GetConstructor(new[] { typeof(IServiceProvider) });
155+
if (ctor != null)
156+
{
157+
return ctor.Invoke(new object[] { this });
158+
}
159+
160+
return null;
161+
}
162+
}
163+
}
164+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.AspNet.OData;
5+
using Microsoft.AspNet.OData.Builder;
6+
using Microsoft.AspNet.OData.Extensions;
7+
using Microsoft.AspNet.OData.Query;
8+
using Microsoft.AspNet.OData.Query.Expressions;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.OData.Edm;
11+
using NHibernate.DomainModel.Northwind.Entities;
12+
using NUnit.Framework;
13+
14+
namespace NHibernate.Test.Linq
15+
{
16+
[TestFixture]
17+
public class ODataTests : LinqTestCase
18+
{
19+
private IEdmModel _edmModel;
20+
21+
protected override void OnSetUp()
22+
{
23+
base.OnSetUp();
24+
25+
_edmModel = CreatEdmModel();
26+
}
27+
28+
[TestCase("$apply=groupby((Customer/CustomerId))", 89)]
29+
[TestCase("$apply=groupby((Customer/CustomerId))&$orderby=Customer/CustomerId", 89)]
30+
[TestCase("$apply=groupby((Customer/CustomerId, ShippingAddress/PostalCode), aggregate(OrderId with average as Average, Employee/EmployeeId with max as Max))", 89)]
31+
[TestCase("$apply=groupby((Customer/CustomerId), aggregate(OrderId with sum as Total))&$skip=2", 87)]
32+
public void OrderGroupBy(string queryString, int expectedRows)
33+
{
34+
var query = ApplyFilter(session.Query<Order>(), queryString);
35+
Assert.That(query, Is.AssignableTo<IQueryable<DynamicTypeWrapper>>());
36+
37+
var results = ((IQueryable<DynamicTypeWrapper>) query).ToList();
38+
Assert.That(results, Has.Count.EqualTo(expectedRows));
39+
}
40+
41+
private IQueryable ApplyFilter<T>(IQueryable<T> query, string queryString)
42+
{
43+
var context = new ODataQueryContext(CreatEdmModel(), typeof(T), null) { };
44+
var dataQuerySettings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False};
45+
var serviceProvider = new ODataServiceProvider(
46+
new Dictionary<System.Type, object>()
47+
{
48+
{typeof(DefaultQuerySettings), new DefaultQuerySettings()},
49+
{typeof(ODataOptions), new ODataOptions()},
50+
{typeof(IEdmModel), _edmModel},
51+
{typeof(ODataQuerySettings), dataQuerySettings},
52+
});
53+
54+
HttpContext httpContext = new DefaultHttpContext();
55+
httpContext.ODataFeature().RequestContainer = serviceProvider;
56+
httpContext.RequestServices = serviceProvider;
57+
var request = httpContext.Request;
58+
Uri requestUri = new Uri($"http://localhost/?{queryString}");
59+
request.Method = HttpMethods.Get;
60+
request.Scheme = requestUri.Scheme;
61+
request.Host = new HostString(requestUri.Host);
62+
request.QueryString = new QueryString(requestUri.Query);
63+
request.Path = new PathString(requestUri.AbsolutePath);
64+
var options = new ODataQueryOptions(context, request);
65+
66+
return options.ApplyTo(query, dataQuerySettings);
67+
}
68+
69+
private static IEdmModel CreatEdmModel()
70+
{
71+
var builder = new ODataConventionModelBuilder();
72+
73+
var adressModel = builder.ComplexType<Address>();
74+
adressModel.Property(o => o.City);
75+
adressModel.Property(o => o.Country);
76+
adressModel.Property(o => o.Fax);
77+
adressModel.Property(o => o.PhoneNumber);
78+
adressModel.Property(o => o.PostalCode);
79+
adressModel.Property(o => o.Region);
80+
adressModel.Property(o => o.Street);
81+
82+
var customerModel = builder.EntitySet<Customer>(nameof(Customer));
83+
customerModel.EntityType.HasKey(o => o.CustomerId);
84+
customerModel.EntityType.Property(o => o.CompanyName);
85+
customerModel.EntityType.Property(o => o.ContactTitle);
86+
customerModel.EntityType.ComplexProperty(o => o.Address);
87+
customerModel.EntityType.HasMany(o => o.Orders);
88+
89+
var orderModel = builder.EntitySet<Order>(nameof(Order));
90+
orderModel.EntityType.HasKey(o => o.OrderId);
91+
orderModel.EntityType.Property(o => o.Freight);
92+
orderModel.EntityType.Property(o => o.OrderDate);
93+
orderModel.EntityType.Property(o => o.RequiredDate);
94+
orderModel.EntityType.Property(o => o.ShippedTo);
95+
orderModel.EntityType.Property(o => o.ShippingDate);
96+
orderModel.EntityType.ComplexProperty(o => o.ShippingAddress);
97+
orderModel.EntityType.HasRequired(o => o.Customer);
98+
orderModel.EntityType.HasOptional(o => o.Employee);
99+
100+
var employeeModel = builder.EntitySet<Employee>(nameof(Employee));
101+
employeeModel.EntityType.HasKey(o => o.EmployeeId);
102+
employeeModel.EntityType.Property(o => o.BirthDate);
103+
employeeModel.EntityType.Property(o => o.Extension);
104+
employeeModel.EntityType.Property(o => o.FirstName);
105+
employeeModel.EntityType.Property(o => o.HireDate);
106+
employeeModel.EntityType.Property(o => o.LastName);
107+
employeeModel.EntityType.Property(o => o.Notes);
108+
employeeModel.EntityType.Property(o => o.Title);
109+
employeeModel.EntityType.HasMany(o => o.Orders);
110+
111+
return builder.GetEdmModel();
112+
}
113+
114+
private class ODataServiceProvider : IServiceProvider
115+
{
116+
private readonly Dictionary<System.Type, object> _singletonObjects = new Dictionary<System.Type, object>();
117+
118+
public ODataServiceProvider(Dictionary<System.Type, object> singletonObjects)
119+
{
120+
_singletonObjects = singletonObjects;
121+
}
122+
123+
public object GetService(System.Type serviceType)
124+
{
125+
if (_singletonObjects.TryGetValue(serviceType, out var service))
126+
{
127+
return service;
128+
}
129+
130+
var ctor = serviceType.GetConstructor(new System.Type[0]);
131+
if (ctor != null)
132+
{
133+
return ctor.Invoke(new object[0]);
134+
}
135+
136+
ctor = serviceType.GetConstructor(new[] { typeof(DefaultQuerySettings) });
137+
if (ctor != null)
138+
{
139+
return ctor.Invoke(new object[] { GetService(typeof(DefaultQuerySettings)) });
140+
}
141+
142+
ctor = serviceType.GetConstructor(new[] { typeof(IServiceProvider) });
143+
if (ctor != null)
144+
{
145+
return ctor.Invoke(new object[] { this });
146+
}
147+
148+
return null;
149+
}
150+
}
151+
}
152+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
</ItemGroup>
4747
<ItemGroup>
4848
<PackageReference Include="log4net" Version="2.0.8" />
49+
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.3.0" />
4950
<PackageReference Include="Microsoft.Data.SqlClient" Version="1.0.19269.1" />
5051
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.109.2" />
5152
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.8.11" />

src/NHibernate/Linq/GroupBy/GroupBySelectClauseRewriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
using System.Linq;
44
using System.Linq.Expressions;
55
using NHibernate.Linq.Expressions;
6+
using NHibernate.Linq.Visitors;
67
using Remotion.Linq;
78
using Remotion.Linq.Clauses;
89
using Remotion.Linq.Clauses.Expressions;
910
using Remotion.Linq.Clauses.ResultOperators;
1011
using Remotion.Linq.Parsing;
11-
using Remotion.Linq.Parsing.ExpressionVisitors;
1212

1313
namespace NHibernate.Linq.GroupBy
1414
{

src/NHibernate/Linq/GroupBy/GroupKeyNominator.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ protected override Expression VisitNew(NewExpression expression)
6161
return Expression.New(expression.Constructor, expression.Arguments.Select(VisitInternal), expression.Members);
6262
}
6363

64+
protected override Expression VisitMemberInit(MemberInitExpression node)
65+
{
66+
_transformed = true;
67+
return Expression.MemberInit((NewExpression) VisitInternal(node.NewExpression), node.Bindings.Select(VisitMemberBinding));
68+
}
69+
70+
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
71+
{
72+
return node.Update(VisitInternal(node.Expression));
73+
}
74+
75+
protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
76+
{
77+
return node.Update(node.Initializers.Select(o => o.Update(o.Arguments.Select(VisitInternal))));
78+
}
79+
80+
protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
81+
{
82+
return node.Update(node.Bindings.Select(VisitMemberBinding));
83+
}
84+
6485
protected override Expression VisitQuerySourceReference(QuerySourceReferenceExpression expression)
6586
{
6687
// If the (sub)expression contains a QuerySourceReference, then the entire expression should be nominated

0 commit comments

Comments
 (0)