Skip to content

Commit

Permalink
CSHARP-5162: Continue to use FindExpressionProjectionDefinition to av…
Browse files Browse the repository at this point in the history
…oid regression against LINQ2.
  • Loading branch information
rstam committed Jul 22, 2024
1 parent e0849de commit 2ab2868
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 19 deletions.
15 changes: 15 additions & 0 deletions src/MongoDB.Driver/ProjectionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,21 @@ public Expression<Func<TSource, TProjection>> Expression

/// <inheritdoc />
public override RenderedProjectionDefinition<TProjection> Render(IBsonSerializer<TSource> sourceSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
{
if (linqProvider == LinqProvider.V2)
{
// this is slightly wrong because we're not actually rendering for a Find
// but this is required to avoid a regression with LINQ2
return RenderForFind(sourceSerializer, serializerRegistry, linqProvider);
}
else
{
return linqProvider.GetAdapter().TranslateExpressionToProjection(_expression, sourceSerializer, serializerRegistry, translationOptions: null);
}
}

/// <inheritdoc />
internal override RenderedProjectionDefinition<TProjection> RenderForFind(IBsonSerializer<TSource> sourceSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
{
return linqProvider.GetAdapter().TranslateExpressionToFindProjection(_expression, sourceSerializer, serializerRegistry);
}
Expand Down
4 changes: 3 additions & 1 deletion src/MongoDB.Driver/ProjectionDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,9 @@ public ProjectionDefinition<TSource> Exclude(Expression<Func<TSource, object>> f
/// </returns>
public ProjectionDefinition<TSource, TProjection> Expression<TProjection>(Expression<Func<TSource, TProjection>> expression)
{
return new ExpressionProjectionDefinition<TSource, TProjection>(expression, null);
// TODO: replace FindExpressionProjectionDefinition with ExpressionProjectionDefinition when LINQ2 is removed
// in the meantime we have to keep using FindExpressionProjectionDefinition here for compatibility with LINQ2
return new FindExpressionProjectionDefinition<TSource, TProjection>(expression);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,69 @@
using System.Linq.Expressions;
using FluentAssertions;
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Linq;
using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests
{
public class FindExpressionProjectionDefinitionTests
{
[Fact]
public void Projection_to_class_should_work()
=> AssertProjection(
[Theory]
[ParameterAttributeData]
public void Projection_to_class_should_work(
[Values(false, true)] bool renderForFind,
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var expectedRenderedProjection = (linqProvider, renderForFind) switch
{
(LinqProvider.V2, _) => "{ A : 1, B : 1, _id : 0 }", // note: result serializer does client-side projection
(LinqProvider.V3, true) => "{ A : 1, X : '$B', _id : 0 }",
(LinqProvider.V3, false) => "{ A : '$A', X : '$B', _id : 0 }",
_ => throw new Exception()
};
AssertProjection(
x => new Projection { A = x.A, X = x.B },
"{ A : 1, X : '$B', _id : 0 }");
renderForFind,
linqProvider,
expectedRenderedProjection);
}

[Fact]
public void Projection_to_anonymous_type_should_work()
=> AssertProjection(
[Theory]
[ParameterAttributeData]
public void Projection_to_anonymous_type_should_work(
[Values(false, true)] bool renderForFind,
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var expectedRenderedProjection = (linqProvider, renderForFind) switch
{
(LinqProvider.V2, _) => "{ A : 1, B : 1, _id : 0 }", // note: result serializer does client-side projection
(LinqProvider.V3, true) => "{ A : 1, X : '$B', _id : 0 }",
(LinqProvider.V3, false) => "{ A : '$A', X : '$B', _id : 0 }",
_ => throw new Exception()
};
AssertProjection(
x => new { x.A, X = x.B },
"{ A : 1, X : '$B', _id : 0 }");
renderForFind,
linqProvider,
expectedRenderedProjection);
}

private void AssertProjection<TProjection>(
Expression<Func<Document, TProjection>> expression,
string expectedProjection)
bool renderForFind,
LinqProvider linqProvider,
string expectedRenderedProjection)
{
var projection = new FindExpressionProjectionDefinition<Document, TProjection>(expression);
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<Document>();

var renderedProjection = projection.Render(
BsonSerializer.LookupSerializer<Document>(),
BsonSerializer.SerializerRegistry);
var renderedProjection = renderForFind ?
projection.RenderForFind(documentSerializer, serializerRegistry, linqProvider) :
projection.Render(documentSerializer, serializerRegistry, linqProvider);

renderedProjection.Document.Should().BeEquivalentTo(expectedProjection);
renderedProjection.Document.Should().BeEquivalentTo(expectedRenderedProjection);
}

private class Document
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,8 @@ public void Find_projection_render_should_work(

var fluentFind = collection.Find(a => a.Id == "1").Project(a => a.Id);

var documentSerializer = collection.DocumentSerializer;
var serializerRegistry = BsonSerializer.SerializerRegistry;
var renderedProjection = fluentFind.Options.Projection.Render(documentSerializer, serializerRegistry, linqProvider);

renderedProjection.Document.Should().Be("{ _id : 1 }");
var renderedProjection = TranslateFindProjection(collection, fluentFind);
renderedProjection.Should().Be("{ _id : 1 }");

var result = fluentFind.Single();
result.Should().Be("1");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/* 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 FluentAssertions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver.Linq;
using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
{
public class CSharp5162Tests : Linq3IntegrationTest
{
[Theory]
[ParameterAttributeData]
public void Builders_Projection_Expression_with_camel_casing_should_work(
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var collection = GetCamelCollection(linqProvider);

var projection = Builders<CamelDocument>.Projection.Expression(x => new CamelDocument { Id = x.Id, Name = x.Name });
var aggregate = collection.Aggregate().Project(projection);

var stages = Translate(collection, aggregate);
if (linqProvider == LinqProvider.V2)
{
AssertStages(stages, "{ $project : { _id : 1, name : 1 } }");
}
else
{
AssertStages(stages, "{ $project : { _id : '$_id', name : '$name' } }");
}

var result = aggregate.ToList().Single();
result.Id.Should().Be(1);
result.Name.Should().Be("John Doe");
}

[Theory]
[ParameterAttributeData]
public void Builders_Projection_Expression_with_pascal_casing_should_work(
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var collection = GetPascalCollection(linqProvider);

var projection = Builders<PascalDocument>.Projection.Expression(x => new PascalDocument { Id = x.Id, Name = x.Name });
var aggregate = collection.Aggregate().Project(projection);

var stages = Translate(collection, aggregate);
if (linqProvider == LinqProvider.V2)
{
stages = NormalizeProjectFieldOrder(stages);
AssertStages(stages, "{ $project : { _id : 1, Name : 1 } }");
}
else
{
AssertStages(stages, "{ $project : { _id : '$_id', Name : '$Name' } }");
}

var result = aggregate.ToList().Single();
result.Id.Should().Be(1);
result.Name.Should().Be("John Doe");
}

[Theory]
[ParameterAttributeData]
public void FindExpressionDefinition_with_camel_casing_should_work(
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var collection = GetCamelCollection(linqProvider);

var projection = new FindExpressionProjectionDefinition<CamelDocument, CamelDocument>(x => new CamelDocument { Id = x.Id, Name = x.Name });
var aggregate = collection.Aggregate().Project(projection);

var stages = Translate(collection, aggregate);
if (linqProvider == LinqProvider.V2)
{
AssertStages(stages, "{ $project : { _id : 1, name : 1 } }");
}
else
{
AssertStages(stages, "{ $project : { _id : '$_id', name : '$name' } }");
}

var result = aggregate.ToList().Single();
result.Id.Should().Be(1);
result.Name.Should().Be("John Doe");
}

[Theory]
[ParameterAttributeData]
public void FindExpressionDefinition_with_pascal_casing_should_work(
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var collection = GetPascalCollection(linqProvider);

var projection = new FindExpressionProjectionDefinition<PascalDocument, PascalDocument>(x => new PascalDocument { Id = x.Id, Name = x.Name });
var aggregate = collection.Aggregate().Project(projection);

var stages = Translate(collection, aggregate);
if (linqProvider == LinqProvider.V2)
{
stages = NormalizeProjectFieldOrder(stages);
AssertStages(stages, "{ $project : { _id : 1, Name : 1 } }");
}
else
{
AssertStages(stages, "{ $project : { _id : '$_id', Name : '$Name' } }");
}

var result = aggregate.ToList().Single();
result.Id.Should().Be(1);
result.Name.Should().Be("John Doe");
}

private IMongoCollection<CamelDocument> GetCamelCollection(LinqProvider linqProvider)
{
var collection = GetCollection<CamelDocument>("test", linqProvider);
var document = new CamelDocument { Id = 1, Name = "John Doe" };
CreateCollection(collection, document);
return collection;
}

private IMongoCollection<PascalDocument> GetPascalCollection(LinqProvider linqProvider)
{
var collection = GetCollection<PascalDocument>("test", linqProvider);
var document = new PascalDocument { Id = 1, Name = "John Doe" };
CreateCollection(collection, document);
return collection;
}

private List<BsonDocument> NormalizeProjectFieldOrder(List<BsonDocument> stages)
{
if (stages.Count == 1 &&
stages[0] is BsonDocument projectStage &&
projectStage.ElementCount == 1 &&
projectStage.GetElement(0).Name == "$project" &&
projectStage[0] is BsonDocument projection &&
projection.ElementCount == 2 &&
projection.Names.SequenceEqual(["Name", "_id"]))
{
stages[0]["$project"] = new BsonDocument
{
{ "_id", projection["_id"] },
{ "Name", projection["Name"] }
};
}

return stages;
}

private class CamelDocument
{
public int Id { get; set; }
[BsonElement("name")] public string Name { get; set; }
[BsonElement("activeSince")] public DateTime ActiveSince { get; set; }
[BsonElement("isActive")] public bool IsActive { get; set; }
}

private class PascalDocument
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime ActiveSince { get; set; }
public bool IsActive { get; set; }
}
}
}

0 comments on commit 2ab2868

Please sign in to comment.