From 8993daa984e6348ba0c2047037713c4fca491555 Mon Sep 17 00:00:00 2001 From: rstam Date: Wed, 25 Jan 2023 17:50:05 -0800 Subject: [PATCH] CSHARP-4453: Support Bucket and BucketAuto stages in LINQ3. --- .../AggregateBucketAutoResultIdSerializer.cs | 103 ++++++++ src/MongoDB.Driver/GroupForLinq3Result.cs | 57 ----- .../IAggregateFluentExtensions.cs | 47 +++- ...zer.cs => AstGroupingPipelineOptimizer.cs} | 90 +++++-- .../Ast/Optimizers/AstPipelineOptimizer.cs | 2 +- .../Ast/Stages/AstBucketAutoStage.cs | 2 +- .../Ast/Stages/AstBucketStage.cs | 2 +- .../GroupExpressionStageDefinitions.cs | 189 --------------- ...ingWithOutputExpressionStageDefinitions.cs | 219 ++++++++++++++++++ .../LinqProviderAdapterV3.cs | 3 +- .../Serializers/IGroupingSerializer.cs | 5 + src/MongoDB.Driver/PipelineDefinition.cs | 7 +- .../PipelineDefinitionBuilder.cs | 62 ++--- src/MongoDB.Driver/PipelineStageDefinition.cs | 40 +++- .../PipelineStageDefinitionBuilder.cs | 64 ++--- .../AggregateFluentBucketAutoTests.cs | 109 ++++++--- .../AggregateFluentBucketTests.cs | 107 ++++++--- .../Jira/CSharp3933Tests.cs | 132 +++-------- .../Linq3TestHelpers.cs | 4 +- .../LinqProviderV3Tests.cs | 11 +- 20 files changed, 741 insertions(+), 514 deletions(-) create mode 100644 src/MongoDB.Driver/AggregateBucketAutoResultIdSerializer.cs delete mode 100644 src/MongoDB.Driver/GroupForLinq3Result.cs rename src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/{AstGroupPipelineOptimizer.cs => AstGroupingPipelineOptimizer.cs} (83%) delete mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/GroupExpressionStageDefinitions.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/GroupingWithOutputExpressionStageDefinitions.cs diff --git a/src/MongoDB.Driver/AggregateBucketAutoResultIdSerializer.cs b/src/MongoDB.Driver/AggregateBucketAutoResultIdSerializer.cs new file mode 100644 index 00000000000..7185dbdd45d --- /dev/null +++ b/src/MongoDB.Driver/AggregateBucketAutoResultIdSerializer.cs @@ -0,0 +1,103 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver +{ + /// + /// Static factory class for AggregateBucketAutoResultIdSerializer. + /// + public static class AggregateBucketAutoResultIdSerializer + { + /// + /// Creates an instance of AggregateBucketAutoResultIdSerializer. + /// + /// The value type. + /// The value serializer. + /// A AggregateBucketAutoResultIdSerializer. + public static IBsonSerializer> Create(IBsonSerializer valueSerializer) + { + return new AggregateBucketAutoResultIdSerializer(valueSerializer); + } + } + + /// + /// A serializer for AggregateBucketAutoResultId. + /// + /// The type of the values. + public class AggregateBucketAutoResultIdSerializer : ClassSerializerBase>, IBsonDocumentSerializer + { + private readonly IBsonSerializer _valueSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// The value serializer. + public AggregateBucketAutoResultIdSerializer(IBsonSerializer valueSerializer) + { + _valueSerializer = Ensure.IsNotNull(valueSerializer, nameof(valueSerializer)); + } + + /// + protected override AggregateBucketAutoResultId DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; + reader.ReadStartDocument(); + TValue min = default; + TValue max = default; + while (reader.ReadBsonType() != 0) + { + var name = reader.ReadName(); + switch (name) + { + case "min": min = _valueSerializer.Deserialize(context); break; + case "max": max = _valueSerializer.Deserialize(context); break; + default: throw new BsonSerializationException($"Invalid element name for AggregateBucketAutoResultId: {name}."); + } + } + reader.ReadEndDocument(); + return new AggregateBucketAutoResultId(min, max); + } + + /// + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, AggregateBucketAutoResultId value) + { + var writer = context.Writer; + writer.WriteStartDocument(); + writer.WriteName("min"); + _valueSerializer.Serialize(context, value.Min); + writer.WriteName("max"); + _valueSerializer.Serialize(context, value.Max); + writer.WriteEndDocument(); + } + + /// + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo) + { + serializationInfo = memberName switch + { + "Min" => new BsonSerializationInfo("min", _valueSerializer, _valueSerializer.ValueType), + "Max" => new BsonSerializationInfo("max", _valueSerializer, _valueSerializer.ValueType), + _ => null + }; + return serializationInfo != null; + } + } +} diff --git a/src/MongoDB.Driver/GroupForLinq3Result.cs b/src/MongoDB.Driver/GroupForLinq3Result.cs deleted file mode 100644 index a5288aa96bc..00000000000 --- a/src/MongoDB.Driver/GroupForLinq3Result.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright 2010-present MongoDB Inc. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -using System.Linq; - -namespace MongoDB.Driver -{ - /// - /// Represents the return result from PipelineDefinitionBuilder.GroupForLinq3 method. - /// - /// The type of the input documents. - /// The type of the values. - /// The type of the output documents. - public class GroupForLinq3Result - { - internal GroupForLinq3Result(PipelineStageDefinition> groupStage, PipelineStageDefinition, TOutput> projectStage) - { - GroupStage = groupStage; - ProjectStage = projectStage; - } - - /// - /// The resulting group stage. - /// - public PipelineStageDefinition> GroupStage { get; } - - /// - /// The resulting project stage. - /// - public PipelineStageDefinition, TOutput> ProjectStage { get; } - - /// - /// Deconstructs this class into its components. - /// - /// The group stage. - /// The project stage. - public void Deconstruct( - out PipelineStageDefinition> groupStage, - out PipelineStageDefinition, TOutput> projectStage) - { - groupStage = GroupStage; - projectStage = ProjectStage; - } - } -} diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index 702f9b65db1..6debce899fd 100644 --- a/src/MongoDB.Driver/IAggregateFluentExtensions.cs +++ b/src/MongoDB.Driver/IAggregateFluentExtensions.cs @@ -95,7 +95,7 @@ public static IAggregateFluent> BucketAuto - /// Appends a $bucketAuto stage to the pipeline. + /// Appends a $bucketAuto stage to the pipeline (this overload can only be used with LINQ3). /// /// The type of the result. /// The type of the value. @@ -110,13 +110,46 @@ public static IAggregateFluent BucketAuto aggregate, Expression> groupBy, int buckets, - Expression, TNewResult>> output, + Expression, TResult>, TNewResult>> output, AggregateBucketAutoOptions options = null) { Ensure.IsNotNull(aggregate, nameof(aggregate)); + if (aggregate.Database.Client.Settings.LinqProvider != LinqProvider.V3) + { + throw new InvalidOperationException("This overload of BucketAuto can only be used with LINQ3."); + } + return aggregate.AppendStage(PipelineStageDefinitionBuilder.BucketAuto(groupBy, buckets, output, options)); } + /// + /// Appends a $bucketAuto stage to the pipeline (this method can only be used with LINQ2). + /// + /// The type of the result. + /// The type of the value. + /// The type of the new result. + /// The aggregate. + /// The expression providing the value to group by. + /// The number of buckets. + /// The output projection. + /// The options (optional). + /// The fluent aggregate interface. + public static IAggregateFluent BucketAutoForLinq2( + this IAggregateFluent aggregate, + Expression> groupBy, + int buckets, + Expression, TNewResult>> output, // the IGrouping for BucketAuto has been wrong all along, only fixing it for LINQ3 + AggregateBucketAutoOptions options = null) + { + Ensure.IsNotNull(aggregate, nameof(aggregate)); + if (aggregate.Database.Client.Settings.LinqProvider != LinqProvider.V2) + { + throw new InvalidOperationException("The BucketAutoForLinq2 method can only be used with LINQ2."); + } + + return aggregate.AppendStage(PipelineStageDefinitionBuilder.BucketAutoForLinq2(groupBy, buckets, output, options)); + } + /// /// Appends a $densify stage to the pipeline. /// @@ -396,15 +429,7 @@ public static IAggregateFluent Group(this IAggregateFluen public static IAggregateFluent Group(this IAggregateFluent aggregate, Expression> id, Expression, TNewResult>> group) { Ensure.IsNotNull(aggregate, nameof(aggregate)); - if (aggregate.Database.Client.Settings.LinqProvider == LinqProvider.V2) - { - return aggregate.AppendStage(PipelineStageDefinitionBuilder.Group(id, group)); - } - else - { - var (groupStage, projectStage) = PipelineStageDefinitionBuilder.GroupForLinq3(id, group); - return aggregate.AppendStage(groupStage).AppendStage(projectStage); - } + return aggregate.AppendStage(PipelineStageDefinitionBuilder.Group(id, group)); } /// diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupPipelineOptimizer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs similarity index 83% rename from src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupPipelineOptimizer.cs rename to src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs index 53f2f991b44..400ebd8522a 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupPipelineOptimizer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs @@ -24,33 +24,42 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers { - internal class AstGroupPipelineOptimizer + internal class AstGroupingPipelineOptimizer { #region static public static AstPipeline Optimize(AstPipeline pipeline) { - var optimizer = new AstGroupPipelineOptimizer(); + var optimizer = new AstGroupingPipelineOptimizer(); for (var i = 0; i < pipeline.Stages.Count; i++) { var stage = pipeline.Stages[i]; - if (stage is AstGroupStage groupStage) + if (IsGroupingStage(stage)) { - pipeline = optimizer.OptimizeGroupStage(pipeline, i, groupStage); + pipeline = optimizer.OptimizeGroupingStage(pipeline, i, stage); } } return pipeline; + + static bool IsGroupingStage(AstStage stage) + { + return stage.NodeType switch + { + AstNodeType.GroupStage or AstNodeType.BucketStage or AstNodeType.BucketAutoStage => true, + _ => false + }; + } } #endregion private readonly AccumulatorSet _accumulators = new AccumulatorSet(); private AstExpression _element; // normally either "$$ROOT" or "$_v" - private AstPipeline OptimizeGroupStage(AstPipeline pipeline, int i, AstGroupStage groupStage) + private AstPipeline OptimizeGroupingStage(AstPipeline pipeline, int i, AstStage groupingStage) { try { - if (IsOptimizableGroupStage(groupStage, out _element)) + if (IsOptimizableGroupingStage(groupingStage, out _element)) { var followingStages = GetFollowingStagesToOptimize(pipeline, i + 1); if (followingStages == null) @@ -58,7 +67,7 @@ private AstPipeline OptimizeGroupStage(AstPipeline pipeline, int i, AstGroupStag return pipeline; } - var mappings = OptimizeGroupAndFollowingStages(groupStage, followingStages); + var mappings = OptimizeGroupingAndFollowingStages(groupingStage, followingStages); if (mappings.Length > 0) { return (AstPipeline)AstNodeReplacer.Replace(pipeline, mappings); @@ -72,23 +81,57 @@ private AstPipeline OptimizeGroupStage(AstPipeline pipeline, int i, AstGroupStag return pipeline; - static bool IsOptimizableGroupStage(AstGroupStage groupStage, out AstExpression element) + static bool IsOptimizableGroupingStage(AstStage groupingStage, out AstExpression element) { - // { $group : { _id : ?, _elements : { $push : element } } } - if (groupStage.Fields.Count == 1) + if (groupingStage is AstGroupStage groupStage) + { + // { $group : { _id : ?, _elements : { $push : element } } } + if (groupStage.Fields.Count == 1) + { + var field = groupStage.Fields[0]; + return IsElementsPush(field, out element); + } + } + + if (groupingStage is AstBucketStage bucketStage) + { + // { $bucket : { groupBy : ?, boundaries : ?, default : ?, output : { _elements : { $push : element } } } } + if (bucketStage.Output.Count == 1) + { + var output = bucketStage.Output[0]; + return IsElementsPush(output, out element); + } + } + + if (groupingStage is AstBucketAutoStage bucketAutoStage) { - var field = groupStage.Fields[0]; - if (field.Path == "_elements" && + // { $bucketAuto : { groupBy : ?, buckets : ?, granularity : ?, output : { _elements : { $push : element } } } } + if (bucketAutoStage.Output.Count == 1) + { + var output = bucketAutoStage.Output[0]; + return IsElementsPush(output, out element); + } + } + + element = null; + return false; + + static bool IsElementsPush(AstAccumulatorField field, out AstExpression element) + { + if ( + field.Path == "_elements" && field.Value is AstUnaryAccumulatorExpression unaryAccumulatorExpression && unaryAccumulatorExpression.Operator == AstUnaryAccumulatorOperator.Push) { element = unaryAccumulatorExpression.Arg; return true; } + else + { + element = null; + return false; + } } - - element = null; - return false; } static List GetFollowingStagesToOptimize(AstPipeline pipeline, int from) @@ -135,7 +178,7 @@ static bool IsLastStageThatCanBeOptimized(AstStage stage) } } - private (AstNode, AstNode)[] OptimizeGroupAndFollowingStages(AstGroupStage groupStage, List followingStages) + private (AstNode, AstNode)[] OptimizeGroupingAndFollowingStages(AstStage groupingStage, List followingStages) { var mappings = new List<(AstNode, AstNode)>(); @@ -148,10 +191,21 @@ static bool IsLastStageThatCanBeOptimized(AstStage stage) } } - var newGroupStage = AstStage.Group(groupStage.Id, _accumulators); - mappings.Add((groupStage, newGroupStage)); + var newGroupingStage = CreateNewGroupingStage(groupingStage, _accumulators); + mappings.Add((groupingStage, newGroupingStage)); return mappings.ToArray(); + + static AstStage CreateNewGroupingStage(AstStage groupingStage, AccumulatorSet accumulators) + { + return groupingStage switch + { + AstGroupStage groupStage => AstStage.Group(groupStage.Id, accumulators), + AstBucketStage bucketStage => AstStage.Bucket(bucketStage.GroupBy, bucketStage.Boundaries, bucketStage.Default, accumulators), + AstBucketAutoStage bucketAutoStage => AstStage.BucketAuto(bucketAutoStage.GroupBy, bucketAutoStage.Buckets, bucketAutoStage.Granularity, accumulators), + _ => throw new Exception($"Unexpected {nameof(groupingStage)} node type: {groupingStage.NodeType}.") + }; + } } private AstStage OptimizeFollowingStage(AstStage stage) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstPipelineOptimizer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstPipelineOptimizer.cs index ccf430221e1..9cc5f8baf67 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstPipelineOptimizer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstPipelineOptimizer.cs @@ -19,7 +19,7 @@ internal static class AstPipelineOptimizer { public static AstPipeline Optimize(AstPipeline pipeline) { - pipeline = AstGroupPipelineOptimizer.Optimize(pipeline); + pipeline = AstGroupingPipelineOptimizer.Optimize(pipeline); pipeline = AstSimplifier.SimplifyAndConvert(pipeline); return pipeline; } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketAutoStage.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketAutoStage.cs index 03bfab8f103..5aba7bf4019 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketAutoStage.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketAutoStage.cs @@ -57,7 +57,7 @@ public override BsonValue Render() { return new BsonDocument { - { "$group", new BsonDocument + { "$bucketAuto", new BsonDocument { { "groupBy", _groupBy.Render() }, { "buckets", _buckets }, diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketStage.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketStage.cs index 30dc82d9b53..6bc4ef5135d 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketStage.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstBucketStage.cs @@ -57,7 +57,7 @@ public override BsonValue Render() { return new BsonDocument { - { "$group", new BsonDocument + { "$bucket", new BsonDocument { { "groupBy", _groupBy.Render() }, { "boundaries", new BsonArray(_boundaries) }, diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/GroupExpressionStageDefinitions.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/GroupExpressionStageDefinitions.cs deleted file mode 100644 index 39b30e36762..00000000000 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/GroupExpressionStageDefinitions.cs +++ /dev/null @@ -1,189 +0,0 @@ -/* Copyright 2010-present MongoDB Inc. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson.Serialization; -using MongoDB.Driver.Core.Misc; -using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers; -using MongoDB.Driver.Linq.Linq3Implementation.Misc; -using MongoDB.Driver.Linq.Linq3Implementation.Reflection; -using MongoDB.Driver.Linq.Linq3Implementation.Translators; -using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators; - -namespace MongoDB.Driver.Linq.Linq3Implementation -{ - internal sealed class GroupExpressionStageDefinitions - { - private readonly Expression> _idExpression; - private readonly Expression, TOutput>> _groupExpression; - private readonly GroupStageDefinition _groupStage; - private readonly ProjectStageDefinition _projectStage; - - public GroupExpressionStageDefinitions( - Expression> idExpression, - Expression, TOutput>> groupExpression) - { - _idExpression = Ensure.IsNotNull(idExpression, nameof(idExpression)); - _groupExpression = Ensure.IsNotNull(groupExpression, nameof(groupExpression)); - - _groupStage = new GroupStageDefinition(idExpression, groupExpression); - _projectStage = new ProjectStageDefinition(_groupStage); - } - - public Expression> IdExpression => _idExpression; - public Expression, TOutput>> GroupExpression => _groupExpression; - public PipelineStageDefinition> GroupStage => _groupStage; - public PipelineStageDefinition, TOutput> ProjectStage => _projectStage; - - private class GroupStageDefinition : PipelineStageDefinition> - { - private readonly Expression> _idExpression; - private readonly Expression, TOutput>> _groupExpression; - private RenderedPipelineStageDefinition _renderedProjectStage = null; - - public GroupStageDefinition( - Expression> idExpression, - Expression, TOutput>> groupExpression) - { - _idExpression = idExpression; - _groupExpression = groupExpression; - } - - public override string OperatorName => "$group"; - public RenderedPipelineStageDefinition RenderedProjectStage => _renderedProjectStage; - - public override RenderedPipelineStageDefinition> Render( - IBsonSerializer inputSerializer, - IBsonSerializerRegistry serializerRegistry, - LinqProvider linqProvider) - { - if (linqProvider != LinqProvider.V3) - { - throw new InvalidOperationException("GroupExpressionStageDefinitions can only be used with LINQ3."); - } - - var expression = CreateExpression(inputSerializer); - expression = PartialEvaluator.EvaluatePartially(expression); - var context = TranslationContext.Create(expression, inputSerializer); - var unoptimizedPipeline = ExpressionToPipelineTranslator.Translate(context, expression); - var pipeline = AstPipelineOptimizer.Optimize(unoptimizedPipeline); - - var groupStageDocument = pipeline.Stages[0].Render().AsBsonDocument; - var renderedGroupStage = new RenderedPipelineStageDefinition>("$group", groupStageDocument, new DummyIGroupingSerializer()); - - var projectStageDocument = pipeline.Stages[1].Render().AsBsonDocument; - _renderedProjectStage = new RenderedPipelineStageDefinition("$project", projectStageDocument, (IBsonSerializer)pipeline.OutputSerializer); - - return renderedGroupStage; - } - - private Expression CreateExpression(IBsonSerializer inputSerializer) - { - var provider = new PseudoQueryProvider(inputSerializer); - var pseudoSource = new PseudoSource(provider); - - var groupByExpression = Expression.Call( - QueryableMethod.GroupByWithKeySelector.MakeGenericMethod(typeof(TInput), typeof(TKey)), - Expression.Constant(pseudoSource), - _idExpression); - - var selectExpression = Expression.Call( - QueryableMethod.Select.MakeGenericMethod(typeof(IGrouping), typeof(TOutput)), - groupByExpression, - _groupExpression); - - return selectExpression; - } - - private class PseudoQueryProvider : IMongoQueryProvider - { - private readonly IBsonSerializer _inputSerializer; - - public PseudoQueryProvider(IBsonSerializer inputSerializer) - { - _inputSerializer = inputSerializer; - } - - public CollectionNamespace CollectionNamespace => throw new NotImplementedException(); - public IBsonSerializer PipelineInputSerializer => _inputSerializer; - - public IQueryable CreateQuery(Expression expression) => throw new NotImplementedException(); - public IQueryable CreateQuery(Expression expression) => throw new NotImplementedException(); - public object Execute(Expression expression) => throw new NotImplementedException(); - public TResult Execute(Expression expression) => throw new NotImplementedException(); - public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public QueryableExecutionModel GetExecutionModel(Expression expression) => throw new NotImplementedException(); - } - - private class PseudoSource : IQueryable - { - private readonly Expression _expression; - private readonly IQueryProvider _provider; - - public PseudoSource(IQueryProvider provider) - { - _provider = provider; - _expression = Expression.Constant(this); - } - - public Type ElementType => typeof(TInput); - - public Expression Expression => _expression; - public IQueryProvider Provider => _provider; - - public IEnumerator GetEnumerator() => throw new NotImplementedException(); - IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); - } - - private class DummyIGroupingSerializer : IBsonSerializer> - { - public Type ValueType => typeof(IGrouping); - - public IGrouping Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) => throw new NotImplementedException(); - public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IGrouping value) => throw new NotImplementedException(); - public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) => throw new NotImplementedException(); - object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) => throw new NotImplementedException(); - } - } - - private class ProjectStageDefinition : PipelineStageDefinition, TOutput> - { - private readonly GroupStageDefinition _groupStageDefinition; - - public ProjectStageDefinition(GroupStageDefinition groupStageDefinition) - { - _groupStageDefinition = groupStageDefinition; - } - - public override string OperatorName => "$project"; - - public override RenderedPipelineStageDefinition Render(IBsonSerializer> inputSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) - { - var renderedProjectStage = _groupStageDefinition.RenderedProjectStage; - if (renderedProjectStage == null) - { - throw new InvalidOperationException("GroupStageDefinition.Render must be called before ProjectStageDefinition.Render."); - } - return renderedProjectStage; - } - } - } -} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/GroupingWithOutputExpressionStageDefinitions.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/GroupingWithOutputExpressionStageDefinitions.cs new file mode 100644 index 00000000000..daf979aa615 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/GroupingWithOutputExpressionStageDefinitions.cs @@ -0,0 +1,219 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq; +using MongoDB.Driver.Linq.Linq3Implementation.Ast; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; +using MongoDB.Driver.Linq.Linq3Implementation.Translators; +using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators; + +namespace MongoDB.Driver.Linq.Linq3Implementation +{ + internal abstract class GroupingWithOutputExpressionStageDefinition : PipelineStageDefinition + { + protected readonly Expression> _output; + + public GroupingWithOutputExpressionStageDefinition(Expression> output) + { + _output = output; + } + + public override RenderedPipelineStageDefinition Render(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) + { + if (linqProvider != LinqProvider.V3) + { + throw new InvalidOperationException($"{GetType().Name} is only intended for use with LINQ3."); + } + + var groupingStage = RenderGroupingStage(inputSerializer, serializerRegistry, out var groupingSerializer); + var projectStage = RenderProjectStage(groupingSerializer, serializerRegistry, out var outputSerializer); + var optimizedStages = OptimizeGroupingStages(groupingStage, projectStage, inputSerializer, outputSerializer); + var renderedStages = optimizedStages.Select(x => x.Render().AsBsonDocument); + + return new RenderedPipelineStageDefinition(OperatorName, renderedStages, outputSerializer); + } + + protected abstract AstStage RenderGroupingStage(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, out IBsonSerializer groupingOutputSerializer); + + private AstStage RenderProjectStage(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, out IBsonSerializer outputSerializer) + { + var partiallyEvaluatedOutput = (Expression>)PartialEvaluator.EvaluatePartially(_output); + var context = TranslationContext.Create(partiallyEvaluatedOutput, inputSerializer); + var outputTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, partiallyEvaluatedOutput, inputSerializer, asRoot: true); + var (projectStage, projectSerializer) = ProjectionHelper.CreateProjectStage(outputTranslation); + outputSerializer = (IBsonSerializer)projectSerializer; + return projectStage; + } + + private IReadOnlyList OptimizeGroupingStages(AstStage groupingStage, AstStage projectStage, IBsonSerializer inputSerializer, IBsonSerializer outputSerializer) + { + var pipeline = AstPipeline.Empty(inputSerializer).AddStages(outputSerializer, groupingStage, projectStage); + var optimizedPipeline = AstPipelineOptimizer.Optimize(pipeline); + return optimizedPipeline.Stages; + } + } + + internal sealed class BucketWithOutputExpressionStageDefinition : GroupingWithOutputExpressionStageDefinition, TOutput> + { + private readonly IReadOnlyList _boundaries; + private readonly Expression> _groupBy; + private readonly AggregateBucketOptions _options; + private readonly ExpressionTranslationOptions _translationOptions; + + public BucketWithOutputExpressionStageDefinition( + Expression> groupBy, + IEnumerable boundaries, + Expression, TOutput>> output, + AggregateBucketOptions options, + ExpressionTranslationOptions translationOptions) + : base(output) + { + _groupBy = groupBy; + _boundaries = boundaries.ToArray(); + _options = options; + _translationOptions = translationOptions; + } + + public override string OperatorName => "$bucket"; + + public override RenderedPipelineStageDefinition Render(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) + { + if (linqProvider == LinqProvider.V2) + { + var linq2Stage = PipelineStageDefinitionBuilder.Bucket( + new ExpressionAggregateExpressionDefinition(_groupBy, _translationOptions), + _boundaries, + new ExpressionBucketOutputProjection(x => default(TValue), _output, _translationOptions), + _options); + return linq2Stage.Render(inputSerializer, serializerRegistry, linqProvider); + } + else + { + return base.Render(inputSerializer, serializerRegistry, linqProvider); + } + } + + protected override AstStage RenderGroupingStage(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, out IBsonSerializer> groupingOutputSerializer) + { + var partiallyEvaluatedGroupBy = (Expression>)PartialEvaluator.EvaluatePartially(_groupBy); + var context = TranslationContext.Create(partiallyEvaluatedGroupBy, inputSerializer); + var groupByTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, partiallyEvaluatedGroupBy, inputSerializer, asRoot: true); + + var valueSerializer = (IBsonSerializer)groupByTranslation.Serializer; + var serializedBoundaries = SerializationHelper.SerializeValues(valueSerializer, _boundaries); + var serializedDefault = _options != null && _options.DefaultBucket.HasValue ? SerializationHelper.SerializeValue(valueSerializer, _options.DefaultBucket.Value) : null; + var pushElements = AstExpression.AccumulatorField("_elements", AstUnaryAccumulatorOperator.Push, AstExpression.Var("ROOT", isCurrent: true)); + groupingOutputSerializer = IGroupingSerializer.Create(valueSerializer, inputSerializer); + + return AstStage.Bucket( + groupByTranslation.Ast, + serializedBoundaries, + serializedDefault, + new[] { pushElements }); + } + } + + internal sealed class BucketAutoWithOutputExpressionStageDefinition : GroupingWithOutputExpressionStageDefinition, TInput>, TOutput> + { + private readonly int _buckets; + private readonly Expression> _groupBy; + private readonly AggregateBucketAutoOptions _options; + + public BucketAutoWithOutputExpressionStageDefinition( + Expression> groupBy, + int buckets, + Expression, TInput>, TOutput>> output, + AggregateBucketAutoOptions options) + : base(output) + { + _groupBy = groupBy; + _buckets = buckets; + _options = options; + } + + public override string OperatorName => "$bucketAuto"; + + protected override AstStage RenderGroupingStage(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, out IBsonSerializer, TInput>> groupingOutputSerializer) + { + var partiallyEvaluatedGroupBy = (Expression>)PartialEvaluator.EvaluatePartially(_groupBy); + var context = TranslationContext.Create(partiallyEvaluatedGroupBy, inputSerializer); + var groupByTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, partiallyEvaluatedGroupBy, inputSerializer, asRoot: true); + + var valueSerializer = (IBsonSerializer)groupByTranslation.Serializer; + var keySerializer = AggregateBucketAutoResultIdSerializer.Create(valueSerializer); + var serializedGranularity = _options != null && _options.Granularity.HasValue ? _options.Granularity.Value.Value : null; + var pushElements = AstExpression.AccumulatorField("_elements", AstUnaryAccumulatorOperator.Push, AstExpression.Var("ROOT", isCurrent: true)); + groupingOutputSerializer = IGroupingSerializer.Create(keySerializer, inputSerializer); + + return AstStage.BucketAuto( + groupByTranslation.Ast, + _buckets, + serializedGranularity, + new[] { pushElements }); + } + } + + internal sealed class GroupWithOutputExpressionStageDefinition : GroupingWithOutputExpressionStageDefinition, TOutput> + { + private readonly Expression> _groupBy; + private readonly ExpressionTranslationOptions _translationOptions; + + public GroupWithOutputExpressionStageDefinition( + Expression> groupBy, + Expression, TOutput>> output, + ExpressionTranslationOptions translationOptions = null) + : base(output) + { + _groupBy = groupBy; + _translationOptions = translationOptions; + } + + public override string OperatorName => "$group"; + + public override RenderedPipelineStageDefinition Render(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) + { + if (linqProvider == LinqProvider.V2) + { + var linq2Stage = PipelineStageDefinitionBuilder.Group(new GroupExpressionProjection(_groupBy, _output, _translationOptions)); + return linq2Stage.Render(inputSerializer, serializerRegistry, linqProvider); + } + else + { + return base.Render(inputSerializer, serializerRegistry, linqProvider); + } + } + + protected override AstStage RenderGroupingStage(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, out IBsonSerializer> groupingOutputSerializer) + { + var partiallyEvaluatedGroupBy = (Expression>)PartialEvaluator.EvaluatePartially(_groupBy); + var context = TranslationContext.Create(partiallyEvaluatedGroupBy, inputSerializer); + var groupByTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, partiallyEvaluatedGroupBy, inputSerializer, asRoot: true); + var pushElements = AstExpression.AccumulatorField("_elements", AstUnaryAccumulatorOperator.Push, AstExpression.Var("ROOT", isCurrent: true)); + var groupBySerializer = (IBsonSerializer)groupByTranslation.Serializer; + groupingOutputSerializer = IGroupingSerializer.Create(groupBySerializer, inputSerializer); + + return AstStage.Group(groupByTranslation.Ast, pushElements); + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs index e8257ccfed2..1c98fbf091a 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs @@ -71,8 +71,7 @@ internal override RenderedProjectionDefinition TranslateExpressionToBuc IBsonSerializerRegistry serializerRegistry, ExpressionTranslationOptions translationOptions) { - // TODO: implement using LINQ3 instead of falling back to LINQ2 - return LinqProviderAdapter.V2.TranslateExpressionToBucketOutputProjection(valueExpression, outputExpression, documentSerializer, serializerRegistry, translationOptions); + throw new InvalidOperationException("TranslateExpressionToBucketOutputProjection can only be used with LINQ2."); } internal override RenderedFieldDefinition TranslateExpressionToField( diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/IGroupingSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/IGroupingSerializer.cs index d8f41451044..fe13ed0a33b 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/IGroupingSerializer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/IGroupingSerializer.cs @@ -107,5 +107,10 @@ public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSeriali var serializerType = typeof(IGroupingSerializer<,>).MakeGenericType(keyType, elementType); return (IBsonSerializer)Activator.CreateInstance(serializerType, keySerializer, elementSerializer); } + + public static IBsonSerializer> Create(IBsonSerializer keySerializer, IBsonSerializer elementSerializer) + { + return new IGroupingSerializer(keySerializer, elementSerializer); + } } } diff --git a/src/MongoDB.Driver/PipelineDefinition.cs b/src/MongoDB.Driver/PipelineDefinition.cs index bac983d9cb6..2d26c40fb22 100644 --- a/src/MongoDB.Driver/PipelineDefinition.cs +++ b/src/MongoDB.Driver/PipelineDefinition.cs @@ -358,9 +358,12 @@ public override RenderedPipelineDefinition Render(IBsonSerializer 0) + foreach (var document in renderedStage.Documents) { - pipeline.Add(renderedStage.Document); + if (document.ElementCount > 0) + { + pipeline.Add(document); + } } } diff --git a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index cd31f0014d2..a78b3f98af1 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -246,7 +246,7 @@ public static PipelineDefinition> Buck } /// - /// Appends a $bucketAuto stage to the pipeline. + /// Appends a $bucketAuto stage to the pipeline (this overload can only be used with LINQ3). /// /// The type of the input documents. /// The type of the intermediate documents. @@ -265,7 +265,7 @@ public static PipelineDefinition BucketAuto pipeline, Expression> groupBy, int buckets, - Expression, TOutput>> output, + Expression, TIntermediate>, TOutput>> output, AggregateBucketAutoOptions options = null, ExpressionTranslationOptions translationOptions = null) { @@ -273,6 +273,34 @@ public static PipelineDefinition BucketAuto + /// Appends a $bucketAuto stage to the pipeline (this method can only be used with LINQ2). + /// + /// The type of the input documents. + /// The type of the intermediate documents. + /// The type of the value. + /// The type of the output documents. + /// The pipeline. + /// The group by expression. + /// The number of buckets. + /// The output projection. + /// The options (optional). + /// The translation options. + /// + /// The fluent aggregate interface. + /// + public static PipelineDefinition BucketAutoForLinq2( + this PipelineDefinition pipeline, + Expression> groupBy, + int buckets, + Expression, TOutput>> output, // the IGrouping for BucketAuto has been wrong all along, only fixing it for LINQ3 + AggregateBucketAutoOptions options = null, + ExpressionTranslationOptions translationOptions = null) + { + Ensure.IsNotNull(pipeline, nameof(pipeline)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.BucketAutoForLinq2(groupBy, buckets, output, options, translationOptions)); + } + /// /// Appends a $changeStream stage to the pipeline. /// Normally you would prefer to use the Watch method of . @@ -756,32 +784,6 @@ public static PipelineDefinition Group - /// Appends a group stage to the pipeline (this method can only be used with LINQ3). - /// - /// The type of the input documents. - /// The type of the intermediate documents. - /// The type of the key. - /// The type of the output documents. - /// The pipeline. - /// The id. - /// The group projection. - /// The translation options. - /// - /// The fluent aggregate interface. - /// - /// This method can only be used with LINQ3 but that can't be verified until Render is called. - public static PipelineDefinition GroupForLinq3( - this PipelineDefinition pipeline, - Expression> id, - Expression, TOutput>> group, - ExpressionTranslationOptions translationOptions = null) - { - Ensure.IsNotNull(pipeline, nameof(pipeline)); - var (groupStage, projectStage) = PipelineStageDefinitionBuilder.GroupForLinq3(id, group, translationOptions); - return pipeline.AppendStage(groupStage).AppendStage(projectStage); - } - /// /// Appends a $limit stage to the pipeline. /// @@ -1558,7 +1560,7 @@ public override RenderedPipelineDefinition Render(IBsonSerializer(documents, outputSerializer); } @@ -1634,7 +1636,7 @@ public override RenderedPipelineDefinition Render(IBsonSerializer(documents, outputSerializer); } diff --git a/src/MongoDB.Driver/PipelineStageDefinition.cs b/src/MongoDB.Driver/PipelineStageDefinition.cs index 42f4430d522..f1f7388a1e2 100644 --- a/src/MongoDB.Driver/PipelineStageDefinition.cs +++ b/src/MongoDB.Driver/PipelineStageDefinition.cs @@ -14,6 +14,8 @@ */ using System; +using System.Collections.Generic; +using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; @@ -39,6 +41,11 @@ public interface IRenderedPipelineStageDefinition /// BsonDocument Document { get; } + /// + /// Gets the documents (usually one but could be more). + /// + IReadOnlyList Documents { get; } + /// /// Gets the output serializer. /// @@ -52,7 +59,7 @@ public interface IRenderedPipelineStageDefinition public class RenderedPipelineStageDefinition : IRenderedPipelineStageDefinition { private string _operatorName; - private BsonDocument _document; + private IReadOnlyList _documents; private IBsonSerializer _outputSerializer; /// @@ -62,16 +69,33 @@ public class RenderedPipelineStageDefinition : IRenderedPipelineStageDe /// The document. /// The output serializer. public RenderedPipelineStageDefinition(string operatorName, BsonDocument document, IBsonSerializer outputSerializer) + : this(operatorName, new List { document }, outputSerializer) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the pipeline operator. + /// The documents. + /// The output serializer. + public RenderedPipelineStageDefinition(string operatorName, IEnumerable documents, IBsonSerializer outputSerializer) { _operatorName = Ensure.IsNotNull(operatorName, nameof(operatorName)); - _document = Ensure.IsNotNull(document, nameof(document)); + _documents = Ensure.IsNotNull(documents, nameof(documents)).ToArray(); _outputSerializer = Ensure.IsNotNull(outputSerializer, nameof(outputSerializer)); } /// public BsonDocument Document { - get { return _document; } + get { return _documents.Single(); } + } + + /// + public IReadOnlyList Documents + { + get { return _documents; } } /// @@ -233,7 +257,15 @@ public string ToString(IBsonSerializer inputSerializer, IBsonSerializerR public string ToString(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) { var renderedStage = Render(inputSerializer, serializerRegistry, linqProvider); - return renderedStage.Document.ToJson(); + var documents = renderedStage.Documents; + if (documents.Count == 1) + { + return documents[0].ToJson(); + } + else + { + return new BsonArray(documents).ToJson(); + } } string IPipelineStageDefinition.ToString(IBsonSerializer inputSerializer, IBsonSerializerRegistry serializerRegistry) diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 70d4c24bd13..49d1b126939 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -22,6 +22,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Linq; +using MongoDB.Driver.Linq.Linq3Implementation; using MongoDB.Driver.Linq.Linq3Implementation.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Translators; using MongoDB.Driver.Search; @@ -174,11 +175,7 @@ public static PipelineStageDefinition Bucket(groupBy, translationOptions), - boundaries, - new ExpressionBucketOutputProjection(x => default(TValue), output, translationOptions), - options); + return new BucketWithOutputExpressionStageDefinition(groupBy, boundaries, output, options, translationOptions); } /// @@ -296,7 +293,7 @@ public static PipelineStageDefinition> } /// - /// Creates a $bucketAuto stage. + /// Creates a $bucketAuto stage (this overload can only be used with LINQ3). /// /// The type of the input documents. /// The type of the output documents. @@ -310,7 +307,31 @@ public static PipelineStageDefinition> public static PipelineStageDefinition BucketAuto( Expression> groupBy, int buckets, - Expression, TOutput>> output, + Expression, TInput>, TOutput>> output, + AggregateBucketAutoOptions options = null, + ExpressionTranslationOptions translationOptions = null) + { + Ensure.IsNotNull(groupBy, nameof(groupBy)); + Ensure.IsNotNull(output, nameof(output)); + return new BucketAutoWithOutputExpressionStageDefinition(groupBy, buckets, output, options); + } + + /// + /// Creates a $bucketAuto stage (this method can only be used with LINQ2). + /// + /// The type of the input documents. + /// The type of the output documents. + /// The type of the output documents. + /// The group by expression. + /// The number of buckets. + /// The output projection. + /// The options (optional). + /// The translation options. + /// The stage. + public static PipelineStageDefinition BucketAutoForLinq2( + Expression> groupBy, + int buckets, + Expression, TOutput>> output, // the IGrouping for BucketAuto has been wrong all along, only fixing it for LINQ3 AggregateBucketAutoOptions options = null, ExpressionTranslationOptions translationOptions = null) { @@ -866,29 +887,7 @@ public static PipelineStageDefinition Group(value, group, translationOptions)); - } - - /// - /// Creates a $group stage (this method can only be used with LINQ3). - /// - /// The type of the input documents. - /// The type of the values. - /// The type of the output documents. - /// The value field. - /// The group projection. - /// The translation options. - /// The stage. - /// This method can only be used with LINQ3 but that can't be verified until Render is called. - public static GroupForLinq3Result GroupForLinq3( - Expression> value, - Expression, TOutput>> group, - ExpressionTranslationOptions translationOptions = null) - { - Ensure.IsNotNull(value, nameof(value)); - Ensure.IsNotNull(group, nameof(group)); - var stages = new Linq.Linq3Implementation.GroupExpressionStageDefinitions(value, group); - return new GroupForLinq3Result(stages.GroupStage, stages.ProjectStage); + return new GroupWithOutputExpressionStageDefinition(value, group); } /// @@ -1899,6 +1898,11 @@ public Expression, TOutput>> OutputExpression public override RenderedProjectionDefinition Render(IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider) { + if (linqProvider != LinqProvider.V2) + { + throw new InvalidOperationException("ExpressionBucketOutputProjection can only be used with LINQ2."); + } + return linqProvider.GetAdapter().TranslateExpressionToBucketOutputProjection(_valueExpression, _outputExpression, documentSerializer, serializerRegistry, _translationOptions); } } diff --git a/tests/MongoDB.Driver.Tests/AggregateFluentBucketAutoTests.cs b/tests/MongoDB.Driver.Tests/AggregateFluentBucketAutoTests.cs index dbf467c6c42..b6936b3d959 100644 --- a/tests/MongoDB.Driver.Tests/AggregateFluentBucketAutoTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateFluentBucketAutoTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using MongoDB.Bson; @@ -21,13 +22,15 @@ using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.TestHelpers; -using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.Linq; +using MongoDB.Driver.Tests.Linq.Linq3ImplementationTests; +using MongoDB.TestHelpers.XunitExtensions; using Xunit; namespace MongoDB.Driver.Tests { - public class AggregateFluentBucketAutoTests + public class AggregateFluentBucketAutoTests : Linq3IntegrationTest { #region static // private static fields @@ -211,48 +214,90 @@ public void BucketAuto_typed_should_return_expected_result() new AggregateBucketAutoResult(1926, 1926, 1)); } - [Fact] - public void BucketAuto_typed_with_output_should_add_expected_stage() + [Theory] + [ParameterAttributeData] + public void BucketAuto_typed_with_output_should_add_expected_stage( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { - var collection = __database.GetCollection(__collectionNamespace.CollectionName); + var collection = GetCollection(linqProvider: linqProvider); var subject = collection.Aggregate(); var buckets = 4; - var result = subject.BucketAuto( - e => e.Year, - buckets, - g => new { _id = default(AggregateBucketAutoResultId), Years = g.Select(e => e.Year), Count = g.Count() }); - - var stage = result.Stages.Single(); - var serializerRegistry = BsonSerializer.SerializerRegistry; - var exhibitSerializer = serializerRegistry.GetSerializer(); - var renderedStage = stage.Render(exhibitSerializer, serializerRegistry); - renderedStage.Document.Should().Be("{ $bucketAuto : { groupBy : \"$year\", buckets : 4, output : { Years : { $push : \"$year\" }, Count : { $sum : 1 } } } }"); + if (linqProvider == LinqProvider.V2) + { + var result = subject.BucketAutoForLinq2( + e => e.Year, + buckets, + g => new { _id = default(AggregateBucketAutoResultId), Years = g.Select(e => e.Year), Count = g.Count() }); + + var stage = result.Stages.Single(); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var exhibitSerializer = serializerRegistry.GetSerializer(); + var renderedStage = stage.Render(exhibitSerializer, serializerRegistry, linqProvider); + renderedStage.Document.Should().Be("{ $bucketAuto : { groupBy : \"$year\", buckets : 4, output : { Years : { $push : \"$year\" }, Count : { $sum : 1 } } } }"); + } + else + { + var result = subject.BucketAuto( + e => (int?)e.Year, + buckets, + g => new { Key = g.Key, Years = g.Select(e => e.Year), Count = g.Count() }); + + var stage = result.Stages.Single(); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var exhibitSerializer = serializerRegistry.GetSerializer(); + var renderedStage = stage.Render(exhibitSerializer, serializerRegistry, linqProvider); + renderedStage.Documents.Should().HaveCount(2); + renderedStage.Documents[0].Should().Be("{ $bucketAuto : { groupBy : '$year', buckets : 4, output : { __agg0 : { $push : '$year' }, __agg1 : { $sum : 1 } } } }"); + renderedStage.Documents[1].Should().Be("{ $project : { Key : '$_id', Years : '$__agg0', Count : '$__agg1', _id : 0 } }"); + } } - [Fact] - public void BucketAuto_typed_with_output_should_return_expected_result() + [Theory] + [ParameterAttributeData] + public void BucketAuto_typed_with_output_should_return_expected_result( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { RequireServer.Check(); EnsureTestData(); - var collection = __database.GetCollection(__collectionNamespace.CollectionName); + var collection = GetCollection(linqProvider: linqProvider); var subject = collection.Aggregate(); var buckets = 4; - var result = subject - .BucketAuto( - e => e.Year, - buckets, - g => new { _id = default(AggregateBucketAutoResultId), Years = g.Select(e => e.Year), Count = g.Count() }) - .ToList(); - - result.Select(r => r._id.Min).Should().Equal(null, 1902, 1925, 1926); - result.Select(r => r._id.Max).Should().Equal(1902, 1925, 1926, 1926); - result[0].Years.Should().Equal(new int[0]); - result[1].Years.Should().Equal(new int[] { 1902 }); - result[2].Years.Should().Equal(new int[] { 1925 }); - result[3].Years.Should().Equal(new int[] { 1926 }); - result.Select(r => r.Count).Should().Equal(1, 1, 1, 1); + if (linqProvider == LinqProvider.V2) + { + var result = subject + .BucketAutoForLinq2( + e => e.Year, + buckets, + g => new { _id = default(AggregateBucketAutoResultId), Years = g.Select(e => e.Year), Count = g.Count() }) + .ToList(); + + result.Select(r => r._id.Min).Should().Equal(null, 1902, 1925, 1926); + result.Select(r => r._id.Max).Should().Equal(1902, 1925, 1926, 1926); + result[0].Years.Should().Equal(new int[0]); + result[1].Years.Should().Equal(new int[] { 1902 }); + result[2].Years.Should().Equal(new int[] { 1925 }); + result[3].Years.Should().Equal(new int[] { 1926 }); + result.Select(r => r.Count).Should().Equal(1, 1, 1, 1); + } + else + { + var result = subject + .BucketAuto( + e => (int?)e.Year, + buckets, + g => new { Key = g.Key, Years = g.Select(e => e.Year), Count = g.Count() }) + .ToList(); + + result.Select(r => r.Key.Min).Should().Equal(null, 1902, 1925, 1926); + result.Select(r => r.Key.Max).Should().Equal(1902, 1925, 1926, 1926); + result[0].Years.Should().Equal(new int[0]); + result[1].Years.Should().Equal(new int[] { 1902 }); + result[2].Years.Should().Equal(new int[] { 1925 }); + result[3].Years.Should().Equal(new int[] { 1926 }); + result.Select(r => r.Count).Should().Equal(1, 1, 1, 1); + } } // nested types diff --git a/tests/MongoDB.Driver.Tests/AggregateFluentBucketTests.cs b/tests/MongoDB.Driver.Tests/AggregateFluentBucketTests.cs index 25b1ac8fa7e..006f493ac8d 100644 --- a/tests/MongoDB.Driver.Tests/AggregateFluentBucketTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateFluentBucketTests.cs @@ -21,14 +21,15 @@ using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.TestHelpers; -using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; +using MongoDB.Driver.Tests.Linq.Linq3ImplementationTests; +using MongoDB.TestHelpers.XunitExtensions; using Xunit; namespace MongoDB.Driver.Tests { - public class AggregateFluentBucketTests + public class AggregateFluentBucketTests : Linq3IntegrationTest { #region static // private static fields @@ -179,50 +180,92 @@ public void Bucket_typed_should_return_expected_result() new AggregateBucketResult("Unknown", 1)); } - [Fact] - public void Bucket_typed_with_output_should_add_expected_stage() + [Theory] + [ParameterAttributeData] + public void Bucket_typed_with_output_should_add_expected_stage( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { - var collection = __database.GetCollection(__collectionNamespace.CollectionName); + var collection = GetCollection(linqProvider: linqProvider); var subject = collection.Aggregate(); var boundaries = new BsonValue[] { 1900, 1920, 1950 }; var options = new AggregateBucketOptions { DefaultBucket = (BsonValue)"Unknown" }; - var result = subject.Bucket( - e => e.Year, - boundaries, - g => new { _id = default(BsonValue), Years = g.Select(e => e.Year), Count = g.Count() }, - options); - - var stage = result.Stages.Single(); - var serializerRegistry = BsonSerializer.SerializerRegistry; - var exhibitSerializer = serializerRegistry.GetSerializer(); - var renderedStage = stage.Render(exhibitSerializer, serializerRegistry); - renderedStage.Document.Should().Be("{ $bucket : { groupBy : \"$year\", boundaries : [ 1900, 1920, 1950 ], default : \"Unknown\", output : { Years : { $push : \"$year\" }, Count : { $sum : 1 } } } }"); + if (linqProvider == LinqProvider.V2) + { + var result = subject.Bucket( + e => e.Year, + boundaries, + g => new { _id = default(BsonValue), Years = g.Select(e => e.Year), Count = g.Count() }, + options); + + var stage = result.Stages.Single(); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var exhibitSerializer = serializerRegistry.GetSerializer(); + var renderedStage = stage.Render(exhibitSerializer, serializerRegistry, linqProvider); + renderedStage.Document.Should().Be("{ $bucket : { groupBy : \"$year\", boundaries : [ 1900, 1920, 1950 ], default : \"Unknown\", output : { Years : { $push : \"$year\" }, Count : { $sum : 1 } } } }"); + } + else + { + var result = subject.Bucket( + e => e.Year, + boundaries, + g => new { Key = g.Key, Years = g.Select(e => e.Year), Count = g.Count() }, + options); + + var stage = result.Stages.Single(); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var exhibitSerializer = serializerRegistry.GetSerializer(); + var renderedStage = stage.Render(exhibitSerializer, serializerRegistry, linqProvider); + renderedStage.Documents.Should().HaveCount(2); + renderedStage.Documents[0].Should().Be("{ $bucket : { groupBy : '$year', boundaries : [ 1900, 1920, 1950 ], default : 'Unknown', output : { __agg0 : {$push : '$year' }, __agg1 : { $sum : 1 } } } }"); + renderedStage.Documents[1].Should().Be("{ $project : { Key : '$_id', Years : '$__agg0', Count : '$__agg1', _id : 0 } }"); + } } - [Fact] - public void Bucket_typed_with_output_should_return_expected_result() + [Theory] + [ParameterAttributeData] + public void Bucket_typed_with_output_should_return_expected_result( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { RequireServer.Check(); EnsureTestData(); - var collection = __database.GetCollection(__collectionNamespace.CollectionName); + var collection = GetCollection(linqProvider: linqProvider); var subject = collection.Aggregate(); var boundaries = new BsonValue[] { 1900, 1920, 1950 }; var options = new AggregateBucketOptions { DefaultBucket = (BsonValue)"Unknown" }; - var result = subject - .Bucket( - e => e.Year, - boundaries, - g => new { _id = default(BsonValue), Years = g.Select(e => e.Year), Count = g.Count() }, - options) - .ToList(); - - result.Select(b => b._id).Should().Equal(1900, 1920, "Unknown"); - result[0].Years.Should().Equal(new[] { 1902 }); - result[1].Years.Should().Equal(new[] { 1926, 1925 }); - result[2].Years.Should().Equal(new int[0]); - result.Select(b => b.Count).Should().Equal(1, 2, 1); + if (linqProvider== LinqProvider.V2) + { + var result = subject + .Bucket( + e => e.Year, + boundaries, + g => new { _id = default(BsonValue), Years = g.Select(e => e.Year), Count = g.Count() }, + options) + .ToList(); + + result.Select(b => b._id).Should().Equal(1900, 1920, "Unknown"); + result[0].Years.Should().Equal(new[] { 1902 }); + result[1].Years.Should().Equal(new[] { 1926, 1925 }); + result[2].Years.Should().Equal(new int[0]); + result.Select(b => b.Count).Should().Equal(1, 2, 1); + } + else + { + var result = subject + .Bucket( + e => e.Year, + boundaries, + g => new { Key = g.Key, Years = g.Select(e => e.Year), Count = g.Count() }, + options) + .ToList(); + + result.Select(b => b.Key).Should().Equal(1900, 1920, "Unknown"); + result[0].Years.Should().Equal(new[] { 1902 }); + result[1].Years.Should().Equal(new[] { 1926, 1925 }); + result[2].Years.Should().Equal(new int[0]); + result.Select(b => b.Count).Should().Equal(1, 2, 1); + } } // nested types diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Jira/CSharp3933Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Jira/CSharp3933Tests.cs index 81813cc458b..f2c9e42a784 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Jira/CSharp3933Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Jira/CSharp3933Tests.cs @@ -15,12 +15,10 @@ using System; using System.Linq; -using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Serializers; -using MongoDB.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; -using MongoDB.Driver.Linq.Linq3Implementation.Serializers; +using MongoDB.TestHelpers.XunitExtensions; using Xunit; namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira @@ -126,56 +124,27 @@ public void PipelineDefinitionBuilder_Group_with_projection_to_TOutput_should_wo Linq3TestHelpers.AssertStages(stages, expectedStages); } - [Fact] - public void PipelineDefinitionBuilder_Group_with_expressions_should_work_with_LINQ2() - { - var emptyPipeline = (PipelineDefinition)new EmptyPipelineDefinition(); - - var pipeline = emptyPipeline.Group(x => 1, x => new { Count = x.Count() }); - - var stages = Linq3TestHelpers.Render(pipeline, BsonDocumentSerializer.Instance, LinqProvider.V2); - var expectedStages = new[] - { - "{ $group : { _id : 1, Count : { $sum : 1 } } }" - }; - Linq3TestHelpers.AssertStages(stages, expectedStages); - } - - [Fact] - public void PipelineDefinitionBuilder_Group_with_expressions_should_throw_with_LINQ3() + [Theory] + [ParameterAttributeData] + public void PipelineDefinitionBuilder_Group_with_expressions_should_work( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { var emptyPipeline = (PipelineDefinition)new EmptyPipelineDefinition(); var pipeline = emptyPipeline.Group(x => 1, x => new { Count = x.Count() }); - var exception = Record.Exception(() => Linq3TestHelpers.Render(pipeline, BsonDocumentSerializer.Instance, LinqProvider.V3)); - exception.Should().BeOfType(); - } - - [Fact] - public void PipelineDefinitionBuilder_GroupForLinq3_with_expressions_should_throw_with_LINQ2() - { - var emptyPipeline = (PipelineDefinition)new EmptyPipelineDefinition(); - - var pipeline = emptyPipeline.GroupForLinq3(x => 1, x => new { Count = x.Count() }); - - var exception = Record.Exception(() => Linq3TestHelpers.Render(pipeline, BsonDocumentSerializer.Instance, LinqProvider.V2)); - exception.Should().BeOfType(); - } - - [Fact] - public void PipelineDefinitionBuilder_GroupForLinq3_with_expressions_should_work_with_LINQ3() - { - var emptyPipeline = (PipelineDefinition)new EmptyPipelineDefinition(); - - var pipeline = emptyPipeline.GroupForLinq3(x => 1, x => new { Count = x.Count() }); - - var stages = Linq3TestHelpers.Render(pipeline, BsonDocumentSerializer.Instance, LinqProvider.V3); - var expectedStages = new[] - { - "{ $group : { _id : 1, __agg0 : { $sum : 1 } } }", - "{ $project : { Count : '$__agg0', _id : 0 } }" - }; + var stages = Linq3TestHelpers.Render(pipeline, BsonDocumentSerializer.Instance, linqProvider); + var expectedStages = linqProvider == LinqProvider.V2 ? + new[] + { + "{ $group : { _id : 1, Count : { $sum : 1 } } }" + } + : + new[] + { + "{ $group : { _id : 1, __agg0 : { $sum : 1 } } }", + "{ $project : { Count : '$__agg0', _id : 0 } }" + }; Linq3TestHelpers.AssertStages(stages, expectedStages); } @@ -186,12 +155,12 @@ public void PipelineStageDefinitionBuilder_Group_with_projection_to_implied_Bson { var stageDefinition = PipelineStageDefinitionBuilder.Group("{ _id : 1, Count : { $sum : 1 } }"); - var stage = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, linqProvider); + var stages = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, linqProvider); var expectedStages = new[] { "{ $group : { _id : 1, Count : { $sum : 1 } } }" }; - Linq3TestHelpers.AssertStages(new[] { stage }, expectedStages); + Linq3TestHelpers.AssertStages(stages, expectedStages); } [Theory] @@ -201,59 +170,34 @@ public void PipelineStageDefinitionBuilder_Group_with_projection_to_TOutput_shou { var stageDefinition = PipelineStageDefinitionBuilder.Group("{ _id : 1, Count : { $sum : 1 } }"); - var stage = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, linqProvider); + var stages = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, linqProvider); var expectedStages = new[] { "{ $group : { _id : 1, Count : { $sum : 1 } } }" }; - Linq3TestHelpers.AssertStages(new[] { stage }, expectedStages); - } - - [Fact] - public void PipelineStageDefinitionBuilder_Group_with_expressions_should_work_with_LINQ2() - { - var stageDefinition = PipelineStageDefinitionBuilder.Group((BsonDocument x) => 1, x => new { Count = x.Count() }); - - var stage = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, LinqProvider.V2); - var expectedStages = new[] - { - "{ $group : { _id : 1, Count : { $sum : 1 } } }" - }; - Linq3TestHelpers.AssertStages(new[] { stage }, expectedStages); + Linq3TestHelpers.AssertStages(stages, expectedStages); } - [Fact] - public void PipelineStageDefinitionBuilder_Group_with_expressions_should_throw_with_LINQ3() + [Theory] + [ParameterAttributeData] + public void PipelineStageDefinitionBuilder_Group_with_expressions_should_work( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) { var stageDefinition = PipelineStageDefinitionBuilder.Group((BsonDocument x) => 1, x => new { Count = x.Count() }); - var exception = Record.Exception(() => Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, LinqProvider.V3)); - exception.Should().BeOfType(); - } - - [Fact] - public void PipelineStageDefinitionBuilder_GroupForLinq3_with_expressions_should_throw_with_LINQ2() - { - var (groupStageDefinition, projectStageDefinition) = PipelineStageDefinitionBuilder.GroupForLinq3((BsonDocument x) => 1, x => new { Count = x.Count() }); - - var exception = Record.Exception(() => Linq3TestHelpers.Render(groupStageDefinition, BsonDocumentSerializer.Instance, LinqProvider.V2)); - exception.Should().BeOfType(); - } - - [Fact] - public void PipelineStageDefinitionBuilderGroupForLinq3_with_expressions_should_work_with_LINQ3() - { - var (groupStageDefinition, projectStageDefinition) = PipelineStageDefinitionBuilder.GroupForLinq3((BsonDocument x) => 1, x => new { Count = x.Count() }); - - var groupStage = Linq3TestHelpers.Render(groupStageDefinition, BsonDocumentSerializer.Instance, LinqProvider.V3); - var groupingSerializer = new IGroupingSerializer(new Int32Serializer(), BsonDocumentSerializer.Instance); - var projectStage = Linq3TestHelpers.Render(projectStageDefinition, groupingSerializer, LinqProvider.V3); - var expectedStages = new[] - { - "{ $group : { _id : 1, __agg0 : { $sum : 1 } } }", - "{ $project : { Count : '$__agg0', _id : 0 } }" - }; - Linq3TestHelpers.AssertStages(new[] { groupStage, projectStage }, expectedStages); + var stages = Linq3TestHelpers.Render(stageDefinition, BsonDocumentSerializer.Instance, linqProvider); + var expectedStages = linqProvider == LinqProvider.V2 ? + new[] + { + "{ $group : { _id : 1, Count : { $sum : 1 } } }" + } + : + new[] + { + "{ $group : { _id : 1, __agg0 : { $sum : 1 } } }", + "{ $project : { Count : '$__agg0', _id : 0 } }" + }; + Linq3TestHelpers.AssertStages(stages, expectedStages); } private IMongoCollection GetCollection(LinqProvider linqProvider) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3TestHelpers.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3TestHelpers.cs index f97b79aaa54..1369d6656de 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3TestHelpers.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/Linq3TestHelpers.cs @@ -37,10 +37,10 @@ public static IList Render(PipelineDefinition(PipelineStageDefinition stage, IBsonSerializer inputSerializer, LinqProvider linqProvider) + public static IReadOnlyList Render(PipelineStageDefinition stage, IBsonSerializer inputSerializer, LinqProvider linqProvider) { var rendered = stage.Render(inputSerializer, BsonSerializer.SerializerRegistry, linqProvider); - return rendered.Document; + return rendered.Documents; } public static List Translate(IMongoCollection collection, IAggregateFluent aggregate) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/LinqProviderV3Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/LinqProviderV3Tests.cs index ec37f9f9eb5..b37de455038 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/LinqProviderV3Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationTests/LinqProviderV3Tests.cs @@ -16,7 +16,6 @@ using System; using System.Linq; using System.Linq.Expressions; -using System.Threading; using FluentAssertions; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; @@ -62,7 +61,7 @@ public void TranslateExpressionToAggregateExpression_should_return_expected_resu } [Fact] - public void TranslateExpressionToBucketOutputProjection_should_return_expected_result() + public void TranslateExpressionToBucketOutputProjection_should_throw() { var subject = LinqProviderAdapter.V3; Expression> valueExpression = c => c.X; @@ -71,13 +70,9 @@ public void TranslateExpressionToBucketOutputProjection_should_return_expected_r var documentSerializer = serializerRegistry.GetSerializer(); var translationOptions = new ExpressionTranslationOptions(); - var result = subject.TranslateExpressionToBucketOutputProjection(valueExpression, outputExpression, documentSerializer, serializerRegistry, translationOptions); + var exception = Record.Exception(() => subject.TranslateExpressionToBucketOutputProjection(valueExpression, outputExpression, documentSerializer, serializerRegistry, translationOptions)); - var expectedResult = LinqProviderAdapter.V2.TranslateExpressionToBucketOutputProjection(valueExpression, outputExpression, documentSerializer, serializerRegistry, translationOptions); - expectedResult.Document.Should().Be("{ $sum : 1 }"); - expectedResult.ProjectionSerializer.Should().BeOfType(); - result.Document.Should().Be(expectedResult.Document); - result.ProjectionSerializer.Should().BeOfType(expectedResult.ProjectionSerializer.GetType()); + exception.Should().BeOfType(); } [Fact]