Skip to content

Commit ddc5823

Browse files
author
rstam
committed
Implemented the Distinct LINQ query operator. Note that because the implementation is based on the Distinct method of MongoCollection the Distinct query operator must be used with a Select that identifies the field whose distinct values are desired.
1 parent 6313309 commit ddc5823

File tree

4 files changed

+262
-14
lines changed

4 files changed

+262
-14
lines changed

Driver/Driver.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
<Compile Include="Linq\Expressions\ExpressionFormatter.cs" />
185185
<Compile Include="Linq\Expressions\ExpressionParameterReplacer.cs" />
186186
<Compile Include="Linq\LinqToMongo.cs" />
187+
<Compile Include="Linq\Translators\DeserializationProjector.cs" />
187188
<Compile Include="Linq\Translators\Nominator.cs" />
188189
<Compile Include="Linq\Translators\OrderByDirection.cs" />
189190
<Compile Include="Linq\Translators\OrderByClause.cs" />
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* Copyright 2010-2012 10gen Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections;
18+
using System.Collections.Generic;
19+
using System.Linq;
20+
using System.Linq.Expressions;
21+
using System.Text;
22+
23+
using MongoDB.Bson;
24+
using MongoDB.Bson.IO;
25+
using MongoDB.Bson.Serialization;
26+
27+
namespace MongoDB.Driver.Linq
28+
{
29+
/// <summary>
30+
/// Represents a projection that deserializes BsonValues.
31+
/// </summary>
32+
/// <typeparam name="TResult">The type of the result objects.</typeparam>
33+
public class DeserializationProjector<TResult> : IEnumerable<TResult>
34+
{
35+
// private fields
36+
private IEnumerable<BsonValue> _source;
37+
private BsonSerializationInfo _serializationInfo;
38+
39+
// constructors
40+
/// <summary>
41+
/// Initializes a new instance of the DeserializationProjector class.
42+
/// </summary>
43+
/// <param name="source">The enumerable object that supplies the source objects.</param>
44+
/// <param name="serializationInfo">Serialization info for deserializing source objects into result objects.</param>
45+
public DeserializationProjector(IEnumerable<BsonValue> source, BsonSerializationInfo serializationInfo)
46+
{
47+
_source = source;
48+
_serializationInfo = serializationInfo;
49+
}
50+
51+
// public methods
52+
/// <summary>
53+
/// Gets an enumerator for the result objects.
54+
/// </summary>
55+
/// <returns></returns>
56+
public IEnumerator<TResult> GetEnumerator()
57+
{
58+
foreach (var value in _source)
59+
{
60+
var document = new BsonDocument("_v", value);
61+
using (var bsonReader = BsonReader.Create(document))
62+
{
63+
bsonReader.ReadStartDocument();
64+
bsonReader.ReadName("_v");
65+
yield return (TResult)_serializationInfo.Serializer.Deserialize(bsonReader, _serializationInfo.NominalType, _serializationInfo.SerializationOptions);
66+
bsonReader.ReadEndDocument();
67+
}
68+
}
69+
}
70+
71+
// explicit interface implementation
72+
IEnumerator IEnumerable.GetEnumerator()
73+
{
74+
return GetEnumerator();
75+
}
76+
}
77+
}

Driver/Linq/Translators/SelectQuery.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class SelectQuery : TranslatedQuery
4444
private Expression _skip;
4545
private Expression _take;
4646
private Func<IEnumerable, object> _elementSelector; // used for First, Last, etc...
47+
private bool _distinct;
4748

4849
// constructors
4950
/// <summary>
@@ -122,6 +123,12 @@ public IMongoQuery BuildQuery()
122123
public override object Execute()
123124
{
124125
var query = BuildQuery();
126+
127+
if (_distinct)
128+
{
129+
return ExecuteDistinct(query);
130+
}
131+
125132
var cursor = Collection.FindAs(DocumentType, query);
126133

127134
if (_orderBy != null)
@@ -207,6 +214,9 @@ public void Translate(Expression expression)
207214
case "LongCount":
208215
TranslateCount(methodCallExpression);
209216
break;
217+
case "Distinct":
218+
TranslateDistinct(methodCallExpression);
219+
break;
210220
case "ElementAt":
211221
case "ElementAtOrDefault":
212222
TranslateElementAt(methodCallExpression);
@@ -818,6 +828,36 @@ private void CombinePredicateWithWhereClause(MethodCallExpression methodCallExpr
818828
}
819829
}
820830

831+
private object ExecuteDistinct(IMongoQuery query)
832+
{
833+
if (_orderBy != null)
834+
{
835+
throw new NotSupportedException("Distinct cannot be used with OrderBy.");
836+
}
837+
if (_skip != null || _take != null)
838+
{
839+
throw new NotSupportedException("Distinct cannot be used with Skip or Take.");
840+
}
841+
if (_projection == null)
842+
{
843+
throw new NotSupportedException("Distinct must be used with Select to identify the field whose distinct values are to be found.");
844+
}
845+
846+
var keyExpression = _projection.Body;
847+
var serializationInfo = GetSerializationInfo(keyExpression);
848+
if (serializationInfo == null)
849+
{
850+
var message = string.Format("Select used with Distinct is not valid: {0}.", ExpressionFormatter.ToString(keyExpression));
851+
throw new NotSupportedException(message);
852+
}
853+
var dottedElementName = serializationInfo.ElementName;
854+
var source = Collection.Distinct(dottedElementName, query);
855+
856+
var deserializationProjectorGenericDefinition = typeof(DeserializationProjector<>);
857+
var deserializationProjectorType = deserializationProjectorGenericDefinition.MakeGenericType(keyExpression.Type);
858+
return Activator.CreateInstance(deserializationProjectorType, source, serializationInfo);
859+
}
860+
821861
private BsonSerializationInfo GetSerializationInfo(Expression expression)
822862
{
823863
var documentSerializer = BsonSerializer.LookupSerializer(DocumentType);
@@ -1014,6 +1054,19 @@ private void TranslateCount(MethodCallExpression methodCallExpression)
10141054
}
10151055
}
10161056

1057+
private void TranslateDistinct(MethodCallExpression methodCallExpression)
1058+
{
1059+
var arguments = methodCallExpression.Arguments.ToArray();
1060+
if (arguments.Length != 1)
1061+
{
1062+
var message = "The version of the Distinct query operator with an equality comparer is not supported.";
1063+
throw new InvalidOperationException(message);
1064+
}
1065+
1066+
Translate(arguments[0]);
1067+
_distinct = true;
1068+
}
1069+
10171070
private void TranslateElementAt(MethodCallExpression methodCallExpression)
10181071
{
10191072
if (methodCallExpression.Arguments.Count != 2)

DriverOnlineTests/Linq/SelectQueryTests.cs

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ public class D
8181
{
8282
[BsonElement("z")]
8383
public int Z; // use field instead of property to test fields also
84+
85+
public override bool Equals(object obj)
86+
{
87+
if (obj == null || GetType() != obj.GetType()) { return false; }
88+
return Z == ((D)obj).Z;
89+
}
90+
91+
public override int GetHashCode()
92+
{
93+
return Z.GetHashCode();
94+
}
8495
}
8596

8697
// used to test some query operators that have an IEqualityComparer parameter
@@ -115,6 +126,11 @@ public int GetHashCode(int obj)
115126
private MongoDatabase _database;
116127
private MongoCollection<C> _collection;
117128
private MongoCollection<SystemProfileInfo> _systemProfileCollection;
129+
private ObjectId id1 = ObjectId.GenerateNewId();
130+
private ObjectId id2 = ObjectId.GenerateNewId();
131+
private ObjectId id3 = ObjectId.GenerateNewId();
132+
private ObjectId id4 = ObjectId.GenerateNewId();
133+
private ObjectId id5 = ObjectId.GenerateNewId();
118134

119135
[TestFixtureSetUp]
120136
public void Setup()
@@ -127,11 +143,11 @@ public void Setup()
127143

128144
// documents inserted deliberately out of order to test sorting
129145
_collection.Drop();
130-
_collection.Insert(new C { X = 2, Y = 11, D = new D { Z = 22 }, A = new [] { 2, 3, 4 }, L = new List<int> { 2, 3, 4 } });
131-
_collection.Insert(new C { X = 1, Y = 11, D = new D { Z = 11 }, S = "x is 1", SA = new string[] { "Tom", "Dick", "Harry" } });
132-
_collection.Insert(new C { X = 3, Y = 33, D = new D { Z = 33 }, B = true, BA = new bool[] { true }, E = E.A, EA = new E[] { E.A, E.B } });
133-
_collection.Insert(new C { X = 5, Y = 44, D = new D { Z = 55 }, DBRef = new MongoDBRef("db", "c", 1) });
134-
_collection.Insert(new C { X = 4, Y = 44, D = new D { Z = 44 } });
146+
_collection.Insert(new C { Id = id2, X = 2, Y = 11, D = new D { Z = 22 }, A = new [] { 2, 3, 4 }, L = new List<int> { 2, 3, 4 } });
147+
_collection.Insert(new C { Id = id1, X = 1, Y = 11, D = new D { Z = 11 }, S = "x is 1", SA = new string[] { "Tom", "Dick", "Harry" } });
148+
_collection.Insert(new C { Id = id3, X = 3, Y = 33, D = new D { Z = 33 }, B = true, BA = new bool[] { true }, E = E.A, EA = new E[] { E.A, E.B } });
149+
_collection.Insert(new C { Id = id5, X = 5, Y = 44, D = new D { Z = 55 }, DBRef = new MongoDBRef("db", "c", 1) });
150+
_collection.Insert(new C { Id = id4, X = 4, Y = 44, D = new D { Z = 44 } });
135151
}
136152

137153
[Test]
@@ -359,15 +375,116 @@ public void TestDefaultIfEmptyWithDefaultValue()
359375
}
360376

361377
[Test]
362-
[ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "The Distinct query operator is not supported.")]
363-
public void TestDistinct()
378+
public void TestDistinctB()
364379
{
365-
var query = _collection.AsQueryable<C>().Distinct();
366-
query.ToList(); // execute query
380+
var query = (from c in _collection.AsQueryable<C>()
381+
select c.B).Distinct();
382+
var results = query.ToList();
383+
Assert.AreEqual(2, results.Count);
384+
Assert.IsTrue(results.Contains(false));
385+
Assert.IsTrue(results.Contains(true));
386+
}
387+
388+
[Test]
389+
public void TestDistinctD()
390+
{
391+
var query = (from c in _collection.AsQueryable<C>()
392+
select c.D).Distinct();
393+
var results = query.ToList(); // execute query
394+
Assert.AreEqual(5, results.Count);
395+
Assert.IsTrue(results.Contains(new D { Z = 11 }));
396+
Assert.IsTrue(results.Contains(new D { Z = 22 }));
397+
Assert.IsTrue(results.Contains(new D { Z = 33 }));
398+
Assert.IsTrue(results.Contains(new D { Z = 44 }));
399+
Assert.IsTrue(results.Contains(new D { Z = 55 }));
400+
}
401+
402+
[Test]
403+
public void TestDistinctDBRef()
404+
{
405+
var query = (from c in _collection.AsQueryable<C>()
406+
select c.DBRef).Distinct();
407+
var results = query.ToList();
408+
Assert.AreEqual(1, results.Count);
409+
Assert.IsTrue(results.Contains(new MongoDBRef("db", "c", 1)));
410+
}
411+
412+
[Test]
413+
public void TestDistinctDZ()
414+
{
415+
var query = (from c in _collection.AsQueryable<C>()
416+
select c.D.Z).Distinct();
417+
var results = query.ToList();
418+
Assert.AreEqual(5, results.Count);
419+
Assert.IsTrue(results.Contains(11));
420+
Assert.IsTrue(results.Contains(22));
421+
Assert.IsTrue(results.Contains(33));
422+
Assert.IsTrue(results.Contains(44));
423+
Assert.IsTrue(results.Contains(55));
424+
}
425+
426+
[Test]
427+
public void TestDistinctE()
428+
{
429+
var query = (from c in _collection.AsQueryable<C>()
430+
select c.E).Distinct();
431+
var results = query.ToList();
432+
Assert.AreEqual(1, results.Count);
433+
Assert.IsTrue(results.Contains(E.A));
434+
}
435+
436+
[Test]
437+
public void TestDistinctId()
438+
{
439+
var query = (from c in _collection.AsQueryable<C>()
440+
select c.Id).Distinct();
441+
var results = query.ToList();
442+
Assert.AreEqual(5, results.Count);
443+
Assert.IsTrue(results.Contains(id1));
444+
Assert.IsTrue(results.Contains(id2));
445+
Assert.IsTrue(results.Contains(id3));
446+
Assert.IsTrue(results.Contains(id4));
447+
Assert.IsTrue(results.Contains(id5));
448+
}
449+
450+
[Test]
451+
public void TestDistinctS()
452+
{
453+
var query = (from c in _collection.AsQueryable<C>()
454+
select c.S).Distinct();
455+
var results = query.ToList();
456+
Assert.AreEqual(1, results.Count);
457+
Assert.IsTrue(results.Contains("x is 1"));
367458
}
368459

369460
[Test]
370-
[ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "The Distinct query operator is not supported.")]
461+
public void TestDistinctX()
462+
{
463+
var query = (from c in _collection.AsQueryable<C>()
464+
select c.X).Distinct();
465+
var results = query.ToList();
466+
Assert.AreEqual(5, results.Count);
467+
Assert.IsTrue(results.Contains(1));
468+
Assert.IsTrue(results.Contains(2));
469+
Assert.IsTrue(results.Contains(3));
470+
Assert.IsTrue(results.Contains(4));
471+
Assert.IsTrue(results.Contains(5));
472+
}
473+
474+
[Test]
475+
public void TestDistinctY()
476+
{
477+
var query = (from c in _collection.AsQueryable<C>()
478+
select c.Y).Distinct();
479+
var results = query.ToList();
480+
Assert.AreEqual(3, results.Count);
481+
Assert.IsTrue(results.Contains(11));
482+
Assert.IsTrue(results.Contains(33));
483+
Assert.IsTrue(results.Contains(44));
484+
}
485+
486+
[Test]
487+
[ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "The version of the Distinct query operator with an equality comparer is not supported.")]
371488
public void TestDistinctWithEqualityComparer()
372489
{
373490
var query = _collection.AsQueryable<C>().Distinct(new CEqualityComparer());
@@ -1237,10 +1354,10 @@ public void TestProjection()
12371354

12381355
Assert.IsNull(selectQuery.BuildQuery());
12391356

1240-
var result = query.ToList();
1241-
Assert.AreEqual(5, result.Count);
1242-
Assert.AreEqual(2, result.First());
1243-
Assert.AreEqual(4, result.Last());
1357+
var results = query.ToList();
1358+
Assert.AreEqual(5, results.Count);
1359+
Assert.AreEqual(2, results.First());
1360+
Assert.AreEqual(4, results.Last());
12441361
}
12451362

12461363
[Test]

0 commit comments

Comments
 (0)