Skip to content

Commit

Permalink
Improved LINQ Select() transformations. Closes GH-1926. Closes GH-1026
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Dec 21, 2023
1 parent 053b023 commit 14068e0
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 154 deletions.
8 changes: 4 additions & 4 deletions docs/documents/querying/linq/projections.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void use_select_with_multiple_fields_to_other_type()
});
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L190-L212' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_other_type_projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L209-L231' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_other_type_projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

When you wish to retrieve certain properties and transform them into an anonymous type:
Expand All @@ -74,7 +74,7 @@ public void use_select_to_transform_to_an_anonymous_type()
.ShouldHaveTheSameElementsAs("Bill", "Hank", "Sam", "Tom");
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L150-L167' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_anonymous_type_projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L169-L186' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_anonymous_type_projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Marten also allows you to run projection queries on deep (nested) properties:
Expand All @@ -96,7 +96,7 @@ public void transform_with_deep_properties()
actual.ShouldHaveTheSameElementsAs(expected);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L302-L317' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_deep_properties_projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L321-L336' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_deep_properties_projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Chaining other Linq Methods
Expand All @@ -121,7 +121,7 @@ public void use_select_to_another_type_with_first()
?.Name.ShouldBe("Bill");
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L78-L94' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_get_first_projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/select_clause_usage.cs#L97-L113' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_get_first_projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## SelectMany()
Expand Down
25 changes: 24 additions & 1 deletion docs/events/projections/async-daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,30 @@ projections are caught up to the latest events posted at the time of the call.
You can see the usage below from one of the Marten tests where we use that method to just wait until the running projection
daemon has caught up:

snippet: sample_using_WaitForNonStaleProjectionDataAsync
<!-- snippet: sample_using_WaitForNonStaleProjectionDataAsync -->
<a id='snippet-sample_using_waitfornonstaleprojectiondataasync'></a>
```cs
[Fact]
public async Task run_simultaneously()
{
StoreOptions(x => x.Projections.Add(new DistanceProjection(), ProjectionLifecycle.Async));

NumberOfStreams = 10;

var agent = await StartDaemon();

// This method publishes a random number of events
await PublishSingleThreaded();

// Wait for all projections to reach the highest event sequence point
// as of the time this method is called
await theStore.WaitForNonStaleProjectionDataAsync(15.Seconds());

await CheckExpectedResults();
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AsyncDaemon.Testing/event_projections_end_to_end.cs#L41-L62' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_waitfornonstaleprojectiondataasync' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The basic idea in your tests is to:

Expand Down
4 changes: 2 additions & 2 deletions docs/events/projections/rebuilding.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class DistanceProjection: EventProjection
}
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AsyncDaemon.Testing/event_projections_end_to_end.cs#L152-L168' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_create_in_event_projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AsyncDaemon.Testing/event_projections_end_to_end.cs#L158-L174' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_create_in_event_projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

<!-- snippet: sample_rebuild-single-projection -->
Expand All @@ -36,5 +36,5 @@ await PublishSingleThreaded();
// rebuild projection `Distance`
await agent.RebuildProjection("Distance", CancellationToken.None);
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AsyncDaemon.Testing/event_projections_end_to_end.cs#L84-L94' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rebuild-single-projection' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AsyncDaemon.Testing/event_projections_end_to_end.cs#L90-L100' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rebuild-single-projection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
2 changes: 1 addition & 1 deletion src/LinqTests/Acceptance/Support/LinqTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected static void selectInOrder<T>(Func<IQueryable<Target>, IQueryable<T>> s
testCases.Add(comparison);
}

private static readonly string[] _methodNames = new string[] { "@where", nameof(ordered), nameof(unordered), nameof(selectInOrder) };
private static readonly string[] _methodNames = new string[] { "@where", nameof(ordered), nameof(unordered), nameof(selectInOrder), "select" };
private static readonly string[] _descriptions;

protected static string[] readDescriptions()
Expand Down
40 changes: 40 additions & 0 deletions src/LinqTests/Acceptance/Support/SelectTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using FastExpressionCompiler;
using Marten;
using Marten.Testing.Documents;
using Marten.Testing.Harness;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Shouldly;

namespace LinqTests.Acceptance.Support;

public class SelectTransform<T>: LinqTestCase
{
private readonly Expression<Func<Target, T>> _selector;

public SelectTransform(Expression<Func<Target, T>> selector)
{
_selector = selector;
}

public override async Task Compare(IQuerySession session, Target[] documents, TestOutputMartenLogger logger)
{
var target = documents.FirstOrDefault(x => x.StringArray?.Length > 0 && x.NumberArray?.Length > 0 && x.Inner != null);
var expected = documents.Select(_selector.CompileFast()).Take(1).Single();

var actual = await session.Query<Target>().Where(x => x.Id == target.Id).Select(_selector).SingleAsync();

var expectedJson = JsonConvert.SerializeObject(expected);
var actualJson = JsonConvert.SerializeObject(actual);

if (!JToken.DeepEquals(JObject.Parse(expectedJson), JObject.Parse(actualJson)))
{
// This would you would assume throw
actualJson.ShouldBe(expectedJson);
}
}
}
19 changes: 19 additions & 0 deletions src/LinqTests/Acceptance/select_clause_usage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ public void use_select_in_query_for_one_field_and_first()
.First().ShouldBe("Bill");
}

[Fact]
public async Task use_select_in_query_for_first_in_collection()
{
theSession.Store(new User { FirstName = "Hank", Roles = new []{"a", "b", "c"}});
theSession.Store(new User { FirstName = "Bill", Roles = new []{"d", "b", "c"} });
theSession.Store(new User { FirstName = "Sam", Roles = new []{"e", "b", "c"} });
theSession.Store(new User { FirstName = "Tom", Roles = new []{"f", "b", "c"} });

await theSession.SaveChangesAsync();

var data = await theSession
.Query<User>()
.OrderBy(x => x.FirstName)
.Select(x => new { Id = x.Id, Role = x.Roles[0] })
.ToListAsync();

data[0].Role.ShouldBe("d");
}

[Fact]
public async Task use_select_in_query_for_one_field_async()
{
Expand Down
85 changes: 85 additions & 0 deletions src/LinqTests/Acceptance/select_clauses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using LinqTests.Acceptance.Support;
using Marten.Testing.Documents;
using Xunit.Abstractions;

namespace LinqTests.Acceptance;

public class select_clauses : LinqTestContext<select_clauses>
{
public select_clauses(DefaultQueryFixture fixture, ITestOutputHelper output) : base(fixture)
{
TestOutput = output;
}

private static void select<T>(Expression<Func<Target, T>> selector)
{
testCases.Add(new SelectTransform<T>(selector));
}

static select_clauses()
{
var number = 10;

select(x => new {Id = x.Id});
select(x => new {Foo = x.Id});
select(x => new {Id = x.Id, Inner = x.Inner});
select(x => new {Id = x.Id, Number = x.Number});
select(x => new {Id = x.Id, Other = x.NumberArray});
select(x => new {Id = x.Id, Other = x.Color});
select(x => new {Id = x.Id, Other = x.Children});
select(x => new {Id = x.Id, Other = x.Date});
select(x => new {Id = x.Id, Other = x.Decimal});
select(x => new {Id = x.Id, Other = x.Double});
select(x => new {Id = x.Id, Other = x.Flag});
select(x => new {Id = x.Id, Other = x.Double});
select(x => new {Id = x.Id, Other = x.Long});
select(x => new {Id = x.Id, Other = x.DateOffset});
select(x => new {Id = x.Id, Other = x.GuidArray});
select(x => new {Id = x.Id, Other = x.GuidDict});
select(x => new {Id = x.Id, Other = x.Float});
select(x => new {Id = x.Id, Other = x.NullableBoolean});
select(x => new {Id = x.Id, Other = x.NullableColor});
select(x => new {Id = x.Id, Other = x.StringArray});
select(x => new {Id = x.Id, Other = x.StringDict});
select(x => new {Id = x.Id, Other = x.TagsHashSet});
select(x => new {Id = x.Id, Name = x.String});
select(x => new {Id = x.Id, Name = "Harold"});
select(x => new {Id = x.Inner.Number, Name = x.Inner.String});
select(x => new {Id = 5, Name = x.Inner.String});
select(x => new {Id = number, Name = x.Inner.String});
select(x => new { Id = x.Id, Name = x.StringArray[0] });
select(x => new { Id = x.Id, Age = x.NumberArray[0] });

select(x => new Person { Age = x.Number, Name = x.String });
select(x => new Person(x.String, x.Number));

select(x => new { Id = x.Id, Person = new Person { Age = x.Number, Name = x.String } });
select(x => new { Id = x.Id, Person = new Person(x.String, x.Number) });
}

[Theory]
[MemberData(nameof(GetDescriptions))]
public Task run_query(string description)
{
return assertTestCase(description, Fixture.Store);
}

public class Person
{
public Person()
{
}

public Person(string name, int age)
{
Name = name;
Age = age;
}

public string Name { get; set; }
public int Age { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace Marten.Linq.Members.ValueCollections;

internal class ValueCollectionMember: QueryableMember, ICollectionMember, IValueCollectionMember
internal class ValueCollectionMember: QueryableMember, ICollectionMember, IValueCollectionMember, ISelectableMember
{
private readonly IQueryableMember _count;
private readonly WholeDataMember _wholeDataMember;
Expand Down Expand Up @@ -194,4 +194,9 @@ public IEnumerator<IQueryableMember> GetEnumerator()
{
throw new NotSupportedException();
}

public void Apply(CommandBuilder builder, ISerializer serializer)
{
builder.Append(RawLocator);
}
}
Loading

0 comments on commit 14068e0

Please sign in to comment.